I've been trying to figure out how to effectively trigger animations in a View when a property in the ViewModel updates, where the animation depends on the value of said property.
I've recreated my problem in a simple application with a single View and ViewModel. The goal here is to transition the color change of a rectangle by using a ColorAnimation. For reference, I've been using the MVVM Foundation package by Josh Smith.
The example project can be downloaded here.
To summarize, I want to animate the color transition in the View whenever the Color property changes.
MainWindow.xaml
<Window x:Class="MVVM.ColorAnimation.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:ColorAnimation="clr-namespace:MVVM.ColorAnimation" Title="MainWindow" Height="350" Width="525">
<Window.DataContext>
<ColorAnimation:MainWindowViewModel />
</Window.DataContext>
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="*" />
<RowDefinition Height="40" />
</Grid.RowDefinitions>
<Rectangle Margin="10">
<Rectangle.Fill>
<SolidColorBrush Color="{Binding Color}"/>
</Rectangle.Fill>
</Rectangle>
<StackPanel Orientation="Horizontal" Grid.Row="1">
<Button Command="{Binding BlueCommand}" Width="100">Blue</Button>
<Button Command="{Binding GreenCommand}" Width="100">Green</Button>
</StackPanel>
</Grid>
</Window>
MainWindowViewModel.cs
namespace MVVM.ColorAnimation
{
using System.Windows.Input;
using System.Windows.Media;
using MVVM;
public class MainWindowViewModel : ObservableObject
{
private ICommand blueCommand;
private ICommand greenCommand;
public ICommand BlueCommand
{
get
{
return this.blueCommand ?? (this.blueCommand = new RelayCommand(this.TurnBlue));
}
}
private void TurnBlue()
{
this.Color = Colors.Blue;
}
public ICommand GreenCommand
{
get
{
return this.greenCommand ?? (this.greenCommand = new RelayCommand(this.TurnGreen));
}
}
private void TurnGreen()
{
this.Color = Colors.Green;
}
private Color color = Colors.Red;
public Color Color
{
get
{
return this.color;
}
set
{
this.color = value;
RaisePropertyChanged("Color");
}
}
}
}
Is there anyway from the View to trigger a ColorAnimation instead of an instant transition between the values? The way I'm currently doing this is another application is quite messy, in that I set the ViewModel through a ViewModel property, and then using a PropertyObserver to monitor value changes, then create the Animation and trigger it from the codebehind:
this.colorObserver = new PropertyObserver<Player>(value)
.RegisterHandler(n => n.Color, this.CreateColorAnimation);
In a situation where I'm dealing with many colors and many potential animations, this becomes quite a mess, and messes up the fact that I'm manually passing in the ViewModel to the View than simply binding the two through a ResourceDictionary. I suppose I could do this in the DataContextChanged event as well, but is there a better way?
If just for a few animations I would recommend using Visual States. Then you can use GoToAction behavior on the view to trigger different animations. If you are dealing with a lot of similar animations, creating your own behavior would be a better solution.
Update
I have created a very simple behaivor to give a Rectangle a little color animation. Here is the code.
public class ColorAnimationBehavior : TriggerAction<FrameworkElement>
{
#region Fill color
[Description("The background color of the rectangle")]
public Color FillColor
{
get { return (Color)GetValue(FillColorProperty); }
set { SetValue(FillColorProperty, value); }
}
public static readonly DependencyProperty FillColorProperty =
DependencyProperty.Register("FillColor", typeof(Color), typeof(ColorAnimationBehavior), null);
#endregion
protected override void Invoke(object parameter)
{
var rect = (Rectangle)AssociatedObject;
var sb = new Storyboard();
sb.Children.Add(CreateVisibilityAnimation(rect, new Duration(new TimeSpan(0, 0, 1)), FillColor));
sb.Begin();
}
private static ColorAnimationUsingKeyFrames CreateVisibilityAnimation(DependencyObject element, Duration duration, Color color)
{
var animation = new ColorAnimationUsingKeyFrames();
animation.KeyFrames.Add(new SplineColorKeyFrame { KeyTime = new TimeSpan(duration.TimeSpan.Ticks), Value = color });
Storyboard.SetTargetProperty(animation, new PropertyPath("(Shape.Fill).(SolidColorBrush.Color)"));
Storyboard.SetTarget(animation, element);
return animation;
}
}
In xaml, you simply attach this behavior like this,
<Rectangle x:Name="rectangle" Fill="Black" Margin="203,103,217,227" Stroke="Black">
<i:Interaction.Triggers>
<i:EventTrigger EventName="MouseLeftButtonDown">
<local:ColorAnimationBehavior FillColor="Red"/>
</i:EventTrigger>
</i:Interaction.Triggers>
</Rectangle>
When you click the Rectangle, it should go from Black color to Red.
I used the code that Xin posted, and made a few very minor tweeks (code is below). The only 3 material differences:
I created the behavior to work on any UIElement, not just a rectangle
I used a PropertyChangedTrigger instead of an EventTrigger. That let's me Monitor the color property on the ViewModel instead of listening for click events.
I bound the FillColor to the Color property of the ViewModel.
To use this, you will need to download the Blend 4 SDK (it's free, and you only need it if you don't already have Expression Blend), and add references to System.Windows.Interactivity, and Microsoft.Expression.Interactions
Here's the code for the behavior class:
// complete code for the animation behavior
using System.Windows;
using System.Windows.Interactivity;
using System.Windows.Media;
using System.Windows.Media.Animation;
namespace ColorAnimationBehavior
{
public class ColorAnimationBehavior: TriggerAction<UIElement>
{
public Color FillColor
{
get { return (Color)GetValue(FillColorProperty); }
set { SetValue(FillColorProperty, value); }
}
public static readonly DependencyProperty FillColorProperty =
DependencyProperty.Register("FillColor", typeof(Color), typeof(ColorAnimationBehavior), null);
public Duration Duration
{
get { return (Duration)GetValue(DurationProperty); }
set { SetValue(DurationProperty, value); }
}
// Using a DependencyProperty as the backing store for Duration. This enables animation, styling, binding, etc...
public static readonly DependencyProperty DurationProperty =
DependencyProperty.Register("Duration", typeof(Duration), typeof(ColorAnimationBehavior), null);
protected override void Invoke(object parameter)
{
var storyboard = new Storyboard();
storyboard.Children.Add(CreateColorAnimation(this.AssociatedObject, this.Duration, this.FillColor));
storyboard.Begin();
}
private static ColorAnimationUsingKeyFrames CreateColorAnimation(UIElement element, Duration duration, Color color)
{
var animation = new ColorAnimationUsingKeyFrames();
animation.KeyFrames.Add(new SplineColorKeyFrame() { KeyTime = duration.TimeSpan, Value = color });
Storyboard.SetTargetProperty(animation, new PropertyPath("(Shape.Fill).(SolidColorBrush.Color)"));
Storyboard.SetTarget(animation, element);
return animation;
}
}
}
Now here's the XAML that hooks it up to your rectangle:
<UserControl x:Class="MVVM.ColorAnimation.Silverlight.MainPage"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:ColorAnimation="clr-namespace:MVVM.ColorAnimation"
xmlns:i="http://schemas.microsoft.com/expression/2010/interactivity"
xmlns:ei="http://schemas.microsoft.com/expression/2010/interactions"
xmlns:ca="clr-namespace:ColorAnimationBehavior;assembly=ColorAnimationBehavior"
mc:Ignorable="d"
d:DesignHeight="300" d:DesignWidth="400">
<UserControl.DataContext>
<ColorAnimation:MainWindowViewModel />
</UserControl.DataContext>
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="*" />
<RowDefinition Height="40" />
</Grid.RowDefinitions>
<Rectangle x:Name="rectangle" Margin="10" Stroke="Black" Fill="Red">
<i:Interaction.Triggers>
<ei:PropertyChangedTrigger Binding="{Binding Color}">
<ca:ColorAnimationBehavior FillColor="{Binding Color}" Duration="0:0:0.5" />
</ei:PropertyChangedTrigger>
</i:Interaction.Triggers>
</Rectangle>
<StackPanel Orientation="Horizontal" Grid.Row="1">
<Button Command="{Binding BlueCommand}" Width="100" Content="Blue"/>
<Button Command="{Binding GreenCommand}" Width="100" Content="Green"/>
</StackPanel>
</Grid>
</UserControl>
It was really Xin's idea -- I just cleaned it up a bit.
Related
I want to create an animation of a bar (rectangle) going from its current width to 0 which will be used as visualization of a countdown.
In the end it should look somehow like this:
Right now my trigger to start the animation is set in a static class (this part already works).
<Control x:Name="content" Grid.Column="0" Margin="0">
<Control.Template>
<ControlTemplate>
<Grid x:Name="FilledCountdownBar" Width="500" HorizontalAlignment="Left" >
<Rectangle Fill="#FFA4B5BF"/>
</Grid>
<ControlTemplate.Triggers>
<DataTrigger Binding="{Binding Path=(Managers:ActionModeManager.ShowUiTimer)}" Value="true">
<DataTrigger.EnterActions>
<BeginStoryboard>
<Storyboard>
<DoubleAnimation
Storyboard.TargetName="FilledCountdownBar"
Storyboard.TargetProperty="(FrameworkElement.Width)"
To="0" Duration="0:1:0" AutoReverse="False"/>
</Storyboard>
</BeginStoryboard>
</DataTrigger.EnterActions>
</DataTrigger>
</ControlTemplate.Triggers>
</ControlTemplate>
</Control.Template>
</Control>
I got several points which I do not get to work here:
This view will be located in the bottom of a window, which is scaleable. So I do not know the starting width in pixels at the beginning of the animation. I would love to remove the "Width" from the FilledCountdownBar element to let it fill the whole space at the start automatically, but then I cannot animate that value (getting an exception).
When I do not set the "From" property of the animation, then the animation does not reset because there is no start value and the width will remain 0 after the animation finished playing the first time.
I have a property (Duration type) in my viewmodel which I want to bind to the duration of the animation. But it looks like I cannot do that in control templates? An exception is thrown:
Cannot freeze this Storyboard timeline tree for use across threads.
I also tried to use a ProgressBar instead, but I could not get it animating smoothly. There were always small steps visible when changing the value, so like that it is not really an option for me.
Any help is welcome, thanks in advance.
When I need to have dynamic animations that rely on Widths and things like this, I always do them in code as attached behaviors or in custom control code.
This allows you to create a Storyboard in code, set all its dynamic properties and then start it.
In this case, once the animation kicks off, it will be for the size of the control once it starts. If the user resizes the window while it's running, the animation won't dynamically scale itself. However, you can indeed make that happen. I just implemented your simple DoubleAnimation.
Here is a working example for your case:
XAML
<Window x:Class="WpfApp4.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="clr-namespace:WpfApp4"
Title="MainWindow"
Width="800"
Height="450"
UseLayoutRounding="True">
<Window.DataContext>
<local:MainWindowViewModel />
</Window.DataContext>
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="*" />
<RowDefinition Height="Auto" />
</Grid.RowDefinitions>
<Control x:Name="CountDownVisual"
Grid.Row="1"
Height="30"
Margin="0">
<Control.Template>
<ControlTemplate>
<Grid x:Name="RootElement">
<Grid x:Name="CountDownBarRootElement"
local:CountDownBarAnimationBehavior.IsActive="{Binding ShowUiTimer}"
local:CountDownBarAnimationBehavior.ParentElement="{Binding ElementName=RootElement}"
local:CountDownBarAnimationBehavior.TargetElement="{Binding ElementName=CountDownBar}">
<Rectangle x:Name="CountDownBar"
HorizontalAlignment="Left"
Fill="#FFA4B5BF" />
</Grid>
</Grid>
</ControlTemplate>
</Control.Template>
</Control>
</Grid>
</Window>
Attached Behavior
using System;
using System.Windows;
using System.Windows.Media.Animation;
using System.Windows.Threading;
namespace WpfApp4
{
public static class CountDownBarAnimationBehavior
{
private static Storyboard sb;
#region IsActive (DependencyProperty)
public static readonly DependencyProperty IsActiveProperty = DependencyProperty.RegisterAttached("IsActive", typeof(bool), typeof(CountDownBarAnimationBehavior), new FrameworkPropertyMetadata(false, OnIsActiveChanged));
public static bool GetIsActive(DependencyObject obj)
{
return (bool)obj.GetValue(IsActiveProperty);
}
public static void SetIsActive(DependencyObject obj, bool value)
{
obj.SetValue(IsActiveProperty, value);
}
private static void OnIsActiveChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
if (!(d is FrameworkElement control))
{
return;
}
if((bool)e.NewValue)
{
if (GetParentElement(control) != null)
{
StartAnimation(control);
}
else
{
// If IsActive is set to true and the other properties haven't
// been updated yet, defer the animation until render time.
control.Dispatcher?.BeginInvoke((Action) (() => { StartAnimation(control); }), DispatcherPriority.Render);
}
}
else
{
StopAnimation();
}
}
#endregion
#region ParentElement (DependencyProperty)
public static readonly DependencyProperty ParentElementProperty = DependencyProperty.RegisterAttached("ParentElement", typeof(FrameworkElement), typeof(CountDownBarAnimationBehavior), new FrameworkPropertyMetadata(null, OnParentElementChanged));
public static FrameworkElement GetParentElement(DependencyObject obj)
{
return (FrameworkElement)obj.GetValue(ParentElementProperty);
}
public static void SetParentElement(DependencyObject obj, FrameworkElement value)
{
obj.SetValue(ParentElementProperty, value);
}
private static void OnParentElementChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
if(!(d is FrameworkElement fe))
{
return;
}
// You can wire up events here if you want to react to size changes, etc.
}
private static void OnParentElementSizeChanged(object sender, SizeChangedEventArgs e)
{
if (!(sender is FrameworkElement fe))
{
return;
}
if (GetIsActive(fe))
{
StopAnimation();
StartAnimation(fe);
}
}
#endregion
#region TargetElement (DependencyProperty)
public static readonly DependencyProperty TargetElementProperty = DependencyProperty.RegisterAttached("TargetElement", typeof(FrameworkElement), typeof(CountDownBarAnimationBehavior), new FrameworkPropertyMetadata(null));
public static FrameworkElement GetTargetElement(DependencyObject obj)
{
return (FrameworkElement)obj.GetValue(TargetElementProperty);
}
public static void SetTargetElement(DependencyObject obj, FrameworkElement value)
{
obj.SetValue(TargetElementProperty, value);
}
#endregion
private static void StartAnimation(DependencyObject d)
{
var parent = GetParentElement(d);
var target = GetTargetElement(d);
if (parent == null || target == null)
{
return;
}
sb = new Storyboard();
var da = new DoubleAnimation();
Storyboard.SetTarget(da, target);
Storyboard.SetTargetProperty(da, new PropertyPath("Width"));
da.AutoReverse = false;
da.Duration = new Duration(new TimeSpan(0, 1, 0));
da.From = parent.ActualWidth;
da.To = 0d;
sb.Children.Add(da);
sb.Begin();
}
private static void StopAnimation()
{
sb?.Stop();
}
}
}
In my application, I have quite a few grids so I was working on creating a user control that has all of the possible buttons we would use with a grid (Add, Remove, Insert, Move Up, Move Down, etc). I use dependency properties to set the visibility and state (enabled or disabled) of the buttons and I use routed events for the click events.
I'm using Caliburn.Micro in my project so I'm using Message.Attach to specify multiple events and the actions they are attached to. On their own, these work without issue.
The problem I have is when the actions have Can* properties to go with them. So for example, I have Add and Remove methods and CanAdd and CanRemove properties to set their state. If CanAdd or CanRemove is false (and the other is true), both buttons get disabled (probably the whole user control is getting disabled).
I can get around this issue by using properties for the states that don't conform to the Caliburn.Micro naming (ex. CanAddRecord and CanRemoveRecord instead of CanAdd and CanRemove) so it doesn't automatically use them and then set the binding in each of the dependency properties. I would prefer not to do this but will if it's in the only way.
I created a simple example project to show this. The project was written in VS 2015 and uses .NET 4.5.2. I'm using Caliburn.Micro v3.1.0.
The application has a main window with the user control on the top. The user control has Add and Remove buttons. Under the user control is a label that shows a count. The Add button increments the count and the Remove button decrements the count. The Add button is always enabled. The Remove button is only enabled if the count is greater than 0 (so you can't set the count to a negative). The count starts at 1 so both buttons start enabled. If you click Add, both buttons stay enabled. If you click Remove till the count gets to 0, both buttons end up disabled.
Is this by design because the messages are attached to the user control as opposed to the specific buttons? Is there a way around this?
I originally tried putting the Message.Attach statements inside of the user control but when clicking the buttons it seemed to want to bind to Add and Remove methods inside of the user control.
App.xaml
<Application
x:Class="WpfApplication1.App"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="clr-namespace:WpfApplication1">
<Application.Resources>
<ResourceDictionary>
<ResourceDictionary.MergedDictionaries>
<ResourceDictionary>
<local:AppBootstrapper x:Key="Bootstrapper" />
</ResourceDictionary>
</ResourceDictionary.MergedDictionaries>
</ResourceDictionary>
</Application.Resources>
</Application>
AppBootStrapper.cs
using System.Windows;
using Caliburn.Micro;
namespace WpfApplication1
{
public class AppBootstrapper : BootstrapperBase
{
public AppBootstrapper()
{
Initialize();
}
protected override void OnStartup(object sender, StartupEventArgs e)
{
DisplayRootViewFor<MainViewModel>();
}
}
}
GridButtonBar.xaml
<UserControl
x:Class="WpfApplication1.GridButtonBar"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
mc:Ignorable="d"
d:DesignHeight="40" d:DesignWidth="230">
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*" />
<ColumnDefinition Width="*" />
</Grid.ColumnDefinitions>
<Button
x:Name="AddButton"
Content="Add"
Grid.Column="0"
Margin="3"
IsEnabled="{Binding AddButtonIsEnabled}"
Click="Add_OnClick" />
<Button
x:Name="RemoveButton"
Content="Remove"
Grid.Column="1"
Margin="3"
IsEnabled="{Binding RemoveButtonIsEnabled}"
Click="Remove_OnClick" />
</Grid>
</UserControl>
GridButtonBar.xaml.cs
using System.Windows;
using System.Windows.Controls;
namespace WpfApplication1
{
public partial class GridButtonBar : UserControl
{
public static readonly DependencyProperty AddButtonIsEnabledProperty =
DependencyProperty.Register(nameof(AddButtonIsEnabled), typeof(bool), typeof(GridButtonBar),
new UIPropertyMetadata(true));
public static readonly DependencyProperty RemoveButtonIsEnabledProperty =
DependencyProperty.Register(nameof(RemoveButtonIsEnabled), typeof(bool), typeof(GridButtonBar),
new UIPropertyMetadata(true));
public static readonly RoutedEvent AddButtonClickedEvent = EventManager.RegisterRoutedEvent("AddButtonClicked",
RoutingStrategy.Bubble, typeof(RoutedEventHandler), typeof(GridButtonBar));
public static readonly RoutedEvent RemoveButtonClickedEvent =
EventManager.RegisterRoutedEvent("RemoveButtonClicked", RoutingStrategy.Bubble, typeof(RoutedEventHandler),
typeof(GridButtonBar));
public GridButtonBar()
{
InitializeComponent();
}
public bool AddButtonIsEnabled
{
get { return (bool) GetValue(AddButtonIsEnabledProperty); }
set { SetValue(AddButtonIsEnabledProperty, value); }
}
public bool RemoveButtonIsEnabled
{
get { return (bool) GetValue(RemoveButtonIsEnabledProperty); }
set { SetValue(RemoveButtonIsEnabledProperty, value); }
}
public event RoutedEventHandler AddButtonClicked
{
add { AddHandler(AddButtonClickedEvent, value); }
remove { RemoveHandler(AddButtonClickedEvent, value); }
}
public event RoutedEventHandler RemoveButtonClicked
{
add { AddHandler(RemoveButtonClickedEvent, value); }
remove { RemoveHandler(RemoveButtonClickedEvent, value); }
}
private void Add_OnClick(object sender, RoutedEventArgs e)
{
RaiseEvent(new RoutedEventArgs(AddButtonClickedEvent));
}
private void Remove_OnClick(object sender, RoutedEventArgs e)
{
RaiseEvent(new RoutedEventArgs(RemoveButtonClickedEvent));
}
}
}
MainView.xaml
<Window
x:Class="WpfApplication1.MainView"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:local="clr-namespace:WpfApplication1"
xmlns:cal="http://www.caliburnproject.org"
mc:Ignorable="d"
Title="{Binding WindowTitle}"
WindowStyle="SingleBorderWindow"
ResizeMode="NoResize"
Height="140" Width="250">
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="50" />
<RowDefinition Height="*" />
</Grid.RowDefinitions>
<local:GridButtonBar
Grid.Row="0"
cal:Message.Attach="[Event AddButtonClicked] = [Action Add];[Event RemoveButtonClicked] = [Action Remove]" />
<Label
Grid.Row="1"
Margin="5"
BorderBrush="Black"
BorderThickness="1"
HorizontalContentAlignment="Center"
VerticalContentAlignment="Center"
Content="{Binding Count}" />
</Grid>
</Window>
MainViewModel.cs
using Caliburn.Micro;
namespace WpfApplication1
{
public class MainViewModel : PropertyChangedBase
{
private const string WindowTitleDefault = "Caliburn Micro Test";
private int _count = 1;
private string _windowTitle = WindowTitleDefault;
public string WindowTitle
{
get { return _windowTitle; }
set
{
_windowTitle = value;
NotifyOfPropertyChange(() => WindowTitle);
}
}
public int Count
{
get { return _count; }
set
{
_count = value;
NotifyOfPropertyChange(() => Count);
}
}
public bool CanAdd => true;
public bool CanRemove => Count > 0;
public void Add()
{
Count++;
}
public void Remove()
{
Count--;
NotifyOfPropertyChange(() => CanRemove);
}
}
}
I am trying to define a user control for the typical dual list situation (where there are two lists of items side by side and button controls to cause selected items from one to be transferred to the other). I am not very proficient at WPF -- most of what I've learned has been bits and pieces through sites like this. I have learned that I can create custom dependency properties for the control so that I can defer binding of items in the control (buttons, textboxes, etc.) until the control is actually used which is great. However, for my control I am going to have the two lists (probably DataGrids since most of my code, to date, has involved them) but they will require a lot more than binding so what I would like to do is something like this:
<MyUserControl . . . .>
<DataGrid . . . .>
<DataGrid . . . .>
</MyUserControl>
But I have no idea how to make that work. I thought there might be some way I could use ContentControls as a stand-in for the DataGrids and then somehow link the datagrids back to the contentcontrols in the usercontrol but I don't really understand Contentcontrols and none of the examples I found using them seemed to apply at all to what I want to do.
Can anyone point me in the right direction on this? Thank you.
I did some more research and found a promising approach, here:
How to add controls dynamically to a UserControl through user's XAML?
I did a small proof-of-concept project and it worked great so I thought I would share it here. It uses Galasoft's MVVM Light framework.
I created a user control with a textblock, a ContentControl, and a button:
<UserControl x:Class="DataGridInUserControlDemo.UserControls.DGPlusUC"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:ignore="http://www.galasoft.ch/ignore"
mc:Ignorable="d ignore"
x:Name="ControlRoot">
<Grid DataContext="{Binding ElementName=ControlRoot}" Margin="10, 10, 10, 10" MinHeight="300" MinWidth="300">
<Grid.RowDefinitions>
<RowDefinition Height="1*"/>
<RowDefinition Height="5*"/>
<RowDefinition Height="2*"/>
</Grid.RowDefinitions>
<TextBlock Grid.Row="0" Text="{Binding Path=BannerText}" HorizontalAlignment="Center" VerticalAlignment="Center" FontSize="20"/>
<ContentControl Grid.Row="1" Content="{Binding Path=InnerContent}" />
<Button Grid.Row="2" x:Name="DemoButton" Content="{Binding Path=ButtonContent}" Command="{Binding Path=ButtonCommand}" Width="75" Height="25" HorizontalAlignment="Center" VerticalAlignment="Center" />
</Grid>
</UserControl>
The attributes I wish to bind external to the UserControl are themselves bound to custom DependencyProperty(s).
This is the code-behind for the user control containing the DependencyProperty definitions:
using System.Windows;
using System.Windows.Controls;
using System.Windows.Input;
namespace DataGridInUserControlDemo.UserControls
{
public partial class DGPlusUC : UserControl
{
public DGPlusUC()
{
InitializeComponent();
}
public const string BannerTextPropertyName = "BannerText";
public string BannerText
{
get
{
return (string)GetValue(BannerTextProperty);
}
set
{
SetValue(BannerTextProperty, value);
}
}
public static readonly DependencyProperty BannerTextProperty = DependencyProperty.Register(
BannerTextPropertyName,
typeof(string),
typeof(DGPlusUC));
public const string ButtonContentPropertyName = "ButtonContent";
public string ButtonContent
{
get
{
return (string)GetValue(ButtonContentProperty);
}
set
{
SetValue(ButtonContentProperty, value);
}
}
public static readonly DependencyProperty ButtonContentProperty = DependencyProperty.Register(
ButtonContentPropertyName,
typeof(string),
typeof(DGPlusUC));
public const string ButtonCommandPropertyName = "ButtonCommand";
public ICommand ButtonCommand
{
get
{
return (ICommand)GetValue(ButtonCommandProperty);
}
set
{
SetValue(ButtonCommandProperty, value);
}
}
public static readonly DependencyProperty ButtonCommandProperty = DependencyProperty.Register(
ButtonCommandPropertyName,
typeof(ICommand),
typeof(DGPlusUC));
public const string InnerContentPropertyName = "InnerContent";
public UIElement InnerContent
{
get
{
return (UIElement)GetValue(InnerContentProperty);
}
set
{
SetValue(InnerContentProperty, value);
}
}
public static readonly DependencyProperty InnerContentProperty = DependencyProperty.Register(
InnerContentPropertyName,
typeof(UIElement),
typeof(DGPlusUC));
}
}
This is the XAML for the main window:
<Window x:Class="DataGridInUserControlDemo.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:ignore="http://www.galasoft.ch/ignore"
xmlns:demo="clr-namespace:DataGridInUserControlDemo.UserControls"
mc:Ignorable="d ignore"
Height="400"
Width="400"
Title="MVVM Light Application"
DataContext="{Binding Main, Source={StaticResource Locator}}">
<Window.Resources>
<ResourceDictionary>
<ResourceDictionary.MergedDictionaries>
<ResourceDictionary Source="Skins/MainSkin.xaml" />
</ResourceDictionary.MergedDictionaries>
</ResourceDictionary>
</Window.Resources>
<Grid x:Name="LayoutRoot">
<demo:DGPlusUC BannerText="{Binding UCTitle}" ButtonContent="{Binding ButtonText}" ButtonCommand="{Binding ButtonCommand}" HorizontalAlignment="Center" VerticalAlignment="Center">
<demo:DGPlusUC.InnerContent>
<Grid DataContext="{Binding Main, Source={StaticResource Locator}}">
<DataGrid ItemsSource="{Binding Path=DataItems}" AutoGenerateColumns="True" />
</Grid>
</demo:DGPlusUC.InnerContent>
</demo:DGPlusUC>
</Grid>
</Window>
The custom DependencyProperty(s) are used as tags in the control to bind to the properties in the ViewModel.
Note the bracketed demo:DGPlusUC.InnerContent -- this is where the replacement code for the ContentControl goes. The embedded UserControl inherits the Window's datacontext but for some reason this section did not, so, after experimenting with it a bit, I just threw up my hands and declared the DataContext explicitly.
And here is the ViewModel code:
using GalaSoft.MvvmLight;
using GalaSoft.MvvmLight.CommandWpf;
using DataGridInUserControlDemo.Model;
using System.Windows.Input;
using System.Collections.ObjectModel;
namespace DataGridInUserControlDemo.ViewModel
{
public class MainViewModel : ViewModelBase
{
private readonly IDataModel theModel;
private ObservableCollection<DataItem> _DataItems;
public ObservableCollection<DataItem> DataItems
{
get
{
return _DataItems;
}
set
{
if (_DataItems == value)
{
return;
}
var oldValue = _DataItems;
_DataItems = value;
RaisePropertyChanged(() => DataItems, oldValue, value, true);
}
}
private string _ButtonText = "First";
public string ButtonText
{
get
{
return this._ButtonText;
}
set
{
if (this._ButtonText == value)
{
return;
}
var oldValue = this._ButtonText;
this._ButtonText = value;
RaisePropertyChanged(() => ButtonText, oldValue, value, true);
}
}
private string _UCTitle = string.Empty;
public string UCTitle
{
get
{
return this._UCTitle;
}
set
{
if (this._UCTitle == value)
{
return;
}
var oldValue = this._UCTitle;
this._UCTitle = value;
RaisePropertyChanged(() => UCTitle, oldValue, value, true);
}
}
private ICommand _ButtonCommand;
public ICommand ButtonCommand
{
get
{
return this._ButtonCommand;
}
set
{
if (this._ButtonCommand == value)
{
return;
}
var oldValue = this._ButtonCommand;
this._ButtonCommand = value;
RaisePropertyChanged(() => ButtonCommand, oldValue, value, true);
}
}
public MainViewModel(IDataModel model)
{
this.theModel = model;
this._UCTitle = "DataGrid in User Control Demo";
this._DataItems = new ObservableCollection<DataItem>(this.theModel.SomeData);
this._ButtonCommand = new RelayCommand(this.ButtonCmd, () => { return true; }) ;
}
private void ButtonCmd()
{
if (this.ButtonText == "First")
{
this.ButtonText = "Second";
}
else
{
this.ButtonText = "First";
}
}
}
}
Finally, here are the results:
DataGrid in UserControl Demo
I am making a small UWP app in which each page has a similar layout. I have created a custom UserControl following this thread, but I am not able to wrap my head around how to pass an Image Source to the background image of the UserControl.
If I remove the references to bgImage, and only mainContent, the whole thing works as expected.
How do I pass a background image source or URI to a UserControl for use with a control?
UserControl XAML:
<UserControl
x:Class="App1.MainTemplate"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="using:App1"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
mc:Ignorable="d"
d:DesignHeight="300"
d:DesignWidth="400">
<Grid Background="Black">
<!-- Background image grid -->
<Grid Margin="0">
<Grid.Background>
<ImageBrush Opacity="100" ImageSource="{x:Bind bgImage}" Stretch="UniformToFill"/>
</Grid.Background>
<!-- Content grid -->
<Grid Margin="25" x:Name="contentGrid" Opacity="100">
<!-- Darkened insert -->
<Border BorderThickness="1" CornerRadius="8" BorderBrush="Black">
<Rectangle Name="Background" Opacity="0.55" Fill="Black" />
</Border>
<StackPanel VerticalAlignment="Center">
<!-- Content here. -->
<ContentPresenter Content="{x:Bind mainContent}" />
</StackPanel>
</Grid>
</Grid>
</Grid>
</UserControl>
MainTemplate.CS:
public sealed partial class MainTemplate : UserControl
{
public MainTemplate()
{
this.InitializeComponent();
}
public static readonly DependencyProperty bgImageProperty = DependencyProperty.Register("bgImage", typeof(ImageSource), typeof(MainTemplate), new PropertyMetadata(null));
public object bgImage
{
get { return GetValue(bgImageProperty); }
set { SetValue(bgImageProperty, value); }
}
public static readonly DependencyProperty mainContentProperty = DependencyProperty.Register("mainContent", typeof(object), typeof(MainTemplate), new PropertyMetadata(null));
public object mainContent
{
get { return GetValue(mainContentProperty); }
set { SetValue(mainContentProperty, value); }
}
}
And finally, an example from one of the pages I am trying to use (MainPage.xaml):
<local:MainTemplate>
<local:MainTemplate.bgImage>
<Image Source="Assets/backgrounds/tailings 2.jpg"/>
</local:MainTemplate.bgImage>
<local:MainTemplate.mainContent>
<Button
x:Name="app1"
Content=""
HorizontalAlignment="Center"
Click="app1_Click"
Width="426"
Height="134"
Style="{StaticResource noMouseoverHover}"
>
<Button.Background>
<ImageBrush ImageSource="Assets/logos/image.png"/>
</Button.Background>
</Button>
</local:MainTemplate.mainContent>
</local:MainTemplate>
The error I get on compile/run is:
Error Invalid binding path 'bgImage' : Cannot bind type 'System.Object' to 'Windows.UI.Xaml.Media.ImageSource' without a converter App1
As already shown in Matt's answer, the type of the bgImage property should be ImageSource:
public ImageSource bgImage
{
get { return (ImageSource)GetValue(bgImageProperty); }
set { SetValue(bgImageProperty, value); }
}
Besides that, you can't assign an Image control to the property, as you are trying to do here:
<local:MainTemplate>
<local:MainTemplate.bgImage>
<Image Source="Assets/backgrounds/tailings 2.jpg"/>
</local:MainTemplate.bgImage>
...
</local:MainTheme>
Instead, you should assign a BitmapImage, like:
<local:MainTemplate>
<local:MainTemplate.bgImage>
<BitmapImage UriSource="Assets/backgrounds/tailings 2.jpg"/>
</local:MainTemplate.bgImage>
...
</local:MainTheme>
Or shorter, taking advantage of built-in type conversion:
<local:MainTheme bgImage="/Assets/backgrounds/tailings 2.jpg">
...
</local:MainTheme>
In addition to the above, you should also set the x:Bind Mode to OneWay, because the default is OneTime. Later changes to the bgImage property would otherwise be ignored:
<ImageBrush ImageSource="{x:Bind bgImage, Mode=OneWay}" ... />
Finally, there are widely accepted naming conventions regarding property names in .NET. They should start with an uppercase letter, so yours should probably be BgImage.
Don't you just need to set the property to be an ImageSource and not an object
public ImageSource bgImage
{
get { return (ImageSource)GetValue(bgImageProperty); }
set { SetValue(bgImageProperty, value); }
}
I have a stripped down WPF example of a problem I am having in a much larger project. I have a user control called "UserControl1". The data context is set to self so I have dependency properties defined in the code-behind.
In the control I have an ItemsControl. In the ItemsSource I have a CompositeCollection that contains a CollectionContainer and a Line. The Line is there just to prove to myself that I am drawing.
I also have an object called "GraphPen" that contains a PathGeometry dependency property. The CollectionContainer of the user control contains an ObservableCollection of these GraphPens.
Now, I have a "MainWindow" to test the user control. In the MainWindow I have a DispatchTimer and in the Tick event of that timer, I add LineSegments to a PathFigure which has been added to the Figures collection of the PathGeometry of the single instance of the GraphPen.
I expect to see a diagonal line being drawn in parallel to the existing red line, but nothing shows up. If I put a break point at the end of the Tick event handler, I can examine the user control and drill down and see that the line segments do exist. For some reason they are not being rendered. I suspect I have done something wrong in the binding.
I will supply the code below.
GraphPen.cs
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Windows;
using System.Windows.Media;
namespace WpfExampleControlLibrary
{
public class GraphPen : DependencyObject
{
#region Constructor
public GraphPen()
{
PenGeometry = new PathGeometry();
}
#endregion Constructor
#region Dependency Properties
// Line Color
public static PropertyMetadata PenLineColorPropertyMetadata
= new PropertyMetadata(null);
public static DependencyProperty PenLineColorProperty
= DependencyProperty.Register(
"PenLineColor",
typeof(Brush),
typeof(GraphPen),
PenLineColorPropertyMetadata);
public Brush PenLineColor
{
get { return (Brush)GetValue(PenLineColorProperty); }
set { SetValue(PenLineColorProperty, value); }
}
// Line Thickness
public static PropertyMetadata PenLineThicknessPropertyMetadata
= new PropertyMetadata(null);
public static DependencyProperty PenLineThicknessProperty
= DependencyProperty.Register(
"PenLineThickness",
typeof(Int32),
typeof(GraphPen),
PenLineThicknessPropertyMetadata);
public Int32 PenLineThickness
{
get { return (Int32)GetValue(PenLineThicknessProperty); }
set { SetValue(PenLineThicknessProperty, value); }
}
// Pen Geometry
public static PropertyMetadata PenGeometryMetadata = new PropertyMetadata(null);
public static DependencyProperty PenGeometryProperty
= DependencyProperty.Register(
"PenGeometry",
typeof(PathGeometry),
typeof(UserControl1),
PenGeometryMetadata);
public PathGeometry PenGeometry
{
get { return (PathGeometry)GetValue(PenGeometryProperty); }
set { SetValue(PenGeometryProperty, value); }
}
#endregion Dependency Properties
}
}
UserControl1.xaml
<UserControl Name="ExampleControl"
x:Class="WpfExampleControlLibrary.UserControl1"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:local="clr-namespace:WpfExampleControlLibrary"
DataContext="{Binding RelativeSource={RelativeSource Self}}"
mc:Ignorable="d"
d:DesignHeight="300" d:DesignWidth="300">
<UserControl.Resources>
<DataTemplate DataType="{x:Type local:GraphPen}">
<Path Stroke="{Binding Path=PenLineColor}"
StrokeThickness="{Binding Path=PenLineThickness}"
Data="{Binding Path=Geometry}">
</Path>
</DataTemplate>
</UserControl.Resources>
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition/>
</Grid.ColumnDefinitions>
<Grid.RowDefinitions>
<RowDefinition/>
<RowDefinition Height="40"/>
</Grid.RowDefinitions>
<ItemsControl Grid.Column="0" Grid.Row="0">
<ItemsControl.ItemsPanel>
<ItemsPanelTemplate>
<Canvas Background="Aquamarine">
<Canvas.LayoutTransform>
<ScaleTransform ScaleX="1" ScaleY="-1" CenterX=".5" CenterY=".5"/>
</Canvas.LayoutTransform>
</Canvas>
</ItemsPanelTemplate>
</ItemsControl.ItemsPanel>
<ItemsControl.ItemsSource>
<CompositeCollection>
<CollectionContainer
Collection="{
Binding Source={RelativeSource Self},
Path=GraphPens,
Mode=OneWay}"/>
<Line X1="10" Y1="0" X2="200" Y2="180" Stroke="DarkRed" StrokeThickness="2"/>
</CompositeCollection>
</ItemsControl.ItemsSource>
</ItemsControl>
<TextBox x:Name="debug" Grid.Column="0" Grid.Row="1" Text="{Binding Path=DebugText}"/>
</Grid>
</UserControl>
UserControl1.xaml.cs
namespace WpfExampleControlLibrary
{
/// <summary>
/// Interaction logic for UserControl1.xaml
/// </summary>
public partial class UserControl1 : UserControl
{
public UserControl1()
{
InitializeComponent();
GraphPens = new ObservableCollection<GraphPen>();
}
#region Dependency Properties
// Pens
public static PropertyMetadata GraphPenMetadata = new PropertyMetadata(null);
public static DependencyProperty GraphPensProperty
= DependencyProperty.Register(
"GraphPens",
typeof(ObservableCollection<GraphPen>),
typeof(UserControl1),
GraphPenMetadata);
public ObservableCollection<GraphPen> GraphPens
{
get { return (ObservableCollection<GraphPen>)GetValue(GraphPensProperty); }
set { SetValue(GraphPensProperty, value); }
}
// Debug Text
public static PropertyMetadata DebugTextMetadata = new PropertyMetadata(null);
public static DependencyProperty DebugTextProperty
= DependencyProperty.Register(
"DebugText",
typeof(string),
typeof(UserControl1),
DebugTextMetadata);
public string DebugText
{
get { return (string)GetValue(DebugTextProperty); }
set { SetValue(DebugTextProperty, value); }
}
#endregion Dependency Properties
}
}
MainWindow.xaml
<Window x:Class="POC_WPF_UserControlExample.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:Exmpl="clr-namespace:WpfExampleControlLibrary;assembly=WpfExampleControlLibrary"
xmlns:local="clr-namespace:POC_WPF_UserControlExample"
mc:Ignorable="d"
Title="MainWindow" Height="550" Width="550">
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="100"/>
<ColumnDefinition />
<ColumnDefinition Width="100"/>
</Grid.ColumnDefinitions>
<Grid.RowDefinitions>
<RowDefinition Height="100" />
<RowDefinition />
<RowDefinition Height="100" />
</Grid.RowDefinitions>
<Exmpl:UserControl1 Grid.Column="1" Grid.Row="1" x:Name="myExample"/>
</Grid>
</Window>
MainWindow.xaml.cs
namespace POC_WPF_UserControlExample
{
/// <summary>
/// Interaction logic for MainWindow.xaml
/// </summary>
public partial class MainWindow : Window
{
private DispatcherTimer _timer = null;
private GraphPen _graphPen0 = null;
private Int32 _pos = 0;
private PathFigure _pathFigure = null;
public MainWindow()
{
InitializeComponent();
_graphPen0 = new GraphPen();
_graphPen0.PenLineColor = Brushes.DarkGoldenrod;
_graphPen0.PenLineThickness = 2;
myExample.GraphPens.Add(_graphPen0);
_timer = new DispatcherTimer();
_timer.Tick += Timer_Tick;
_timer.Interval = new TimeSpan(0, 0, 0, 0, 100);
_timer.Start();
}
private void Timer_Tick(object sender, EventArgs e)
{
_pos++;
Point penPoint0 = new Point(_pos, _pos + 20);
if (_graphPen0.PenGeometry.Figures.Count == 0)
{
_pathFigure = new PathFigure();
_graphPen0.PenGeometry.Figures.Add(_pathFigure);
_pathFigure.StartPoint = penPoint0;
}
else
{
LineSegment segment = new LineSegment(penPoint0, false);
_pathFigure.Segments.Add(segment);
}
myExample.DebugText = _pos.ToString();
}
}
}
Screen Shot
I did not need INotifyPropertyChanged, and I did not need to recreate PenGeometry. I'm sorry I wasted your time with those ideas.
I've got your code drawing... something. A line that grows. I don't know if it's drawing exactly what you want, but you can figure out that part now that you can see what it is drawing.
First, minor copy/paste error in GraphPen.cs:
public static DependencyProperty PenGeometryProperty
= DependencyProperty.Register(
"PenGeometry",
typeof(PathGeometry),
typeof(UserControl1),
PenGeometryMetadata);
Owner type parameter needs to be GraphPen, not UserControl1:
typeof(PathGeometry),
typeof(GraphPen),
PenGeometryMetadata);
Second: Binding in UserControl1. Your binding to Self isn't going to work because Self, in that case, is the CollectionContainer you're binding on. Usually you'd use a source of RelativeSource={RelativeSource AncestorType=UserControl}, but CollectionContainer is not in the visual tree so that doesn't work (real intuitive, huh?). Instead we use a BindingProxy (source to follow):
<UserControl.Resources>
<!-- ... stuff ... -->
<local:BindingProxy
x:Key="UserControlBindingProxy"
Data="{Binding RelativeSource={RelativeSource AncestorType=UserControl}}"
/>
</UserControl.Resources>
And for the collection container...
<CollectionContainer
Collection="{
Binding
Source={StaticResource UserControlBindingProxy},
Path=Data.GraphPens,
Mode=OneWay}"
/>
Notice we're binding to Data.GraphPens; Data is the target of the proxy.
Also, we need an ItemTemplate for the ItemsControl, because it doesn't know how to display a GraphPen:
<ItemsControl.ItemTemplate>
<DataTemplate DataType="local:GraphPen">
<Border >
<Path
Data="{Binding PenGeometry}"
StrokeThickness="{Binding PenLineThickness}"
Stroke="{Binding PenLineColor, PresentationTraceSources.TraceLevel=None}"
/>
</Border>
</DataTemplate>
</ItemsControl.ItemTemplate>
Note PresentationTraceSources.TraceLevel=None. Change None to High and it'll give you a lot of debugging info about the Binding in the VS Output pane. I added that when I was trying to figure out why I was setting PenLineColor to Brushes.Black in the constructor, but it kept coming out DarkGoldenrod at runtime. Can you say duhhh? Duhhh!
Now the binding proxy. What that does is you take any random object you want to use as a DataContext, bind it to Data on a BindingProxy instance defined as a resource, and you've got that DataContext available via a resource that you can get to with a StaticResource. If you're someplace where you can't get to something via the visual tree with RelativeSource, it's an option that you can rely on.
BindingProxy.cs
using System.Windows;
namespace WpfExampleControlLibrary
{
public class BindingProxy : Freezable
{
#region Overrides of Freezable
protected override Freezable CreateInstanceCore()
{
return new BindingProxy();
}
#endregion
public object Data
{
get { return (object)GetValue(DataProperty); }
set { SetValue(DataProperty, value); }
}
// Using a DependencyProperty as the backing store for Data. This enables animation, styling, binding, etc...
public static readonly DependencyProperty DataProperty =
DependencyProperty.Register("Data", typeof(object), typeof(BindingProxy), new UIPropertyMetadata(null));
}
}
And finally, in MainWindow, you need to pass true for isStroked on the LineSegment instances:
LineSegment segment = new LineSegment(penPoint0, true);
Otherwise they're not drawn.
So now it's back in your lap, in warm goldenrod and soothing aquamarine. Ave, imperator, and all that.
Edit by original author!
Thank you Ed for all your effort.
I can't believe I missed the UserControl1 -> GraphPen
The BindingProxy will be very handy
The TraceLevel will also be handy, I had not used that before
I also corrected the DebugText binding and now that works
I never even noticed that!
In the DataTemplate why did we need to wrap the Path in a Border?
We don't. Before I got the Path showing, I added that to have something in the template that was guaranteed to be visible . It had a green border originally. I removed those attributes, but forgot to remove the Border itself.
If you look in my Timer_Tick you will note that now all I have to update is adding new segments. Hopefully, this will help performance. Your opinion?
No idea. I would actually put that segment adding code in GraphPen as AddSegment(Point pt) and AddSegment(float x, float y) => AddSegment(new Point(x,y)); overloads. I have a great allergy to putting logic in event handlers. The most I'll do is toss an if or a try/catch around a non-handler method that does the real work. Then I'd write AddSegment(Point pt) both ways and benchmark one against the other.
I will add my code for completeness:
UserControl1.xaml
<UserControl Name="ExampleControl"
x:Class="WpfExampleControlLibrary.UserControl1"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:local="clr-namespace:WpfExampleControlLibrary"
mc:Ignorable="d"
d:DesignHeight="300" d:DesignWidth="300">
<UserControl.Resources>
<local:BindingProxy
x:Key="UserControlBindingProxy"
Data="{Binding RelativeSource={RelativeSource AncestorType=UserControl}}"/>
<DataTemplate DataType="{x:Type local:GraphPen}">
<Border>
<Path
Data="{Binding PenGeometry}"
StrokeThickness="{Binding PenLineThickness}"
Stroke="{Binding
PenLineColor,
PresentationTraceSources.TraceLevel=None}"
/>
</Border>
</DataTemplate>
</UserControl.Resources>
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition/>
</Grid.ColumnDefinitions>
<Grid.RowDefinitions>
<RowDefinition/>
<RowDefinition Height="40"/>
</Grid.RowDefinitions>
<ItemsControl Grid.Column="0" Grid.Row="0">
<ItemsControl.ItemsPanel>
<ItemsPanelTemplate>
<Canvas Background="Aquamarine">
<Canvas.LayoutTransform>
<ScaleTransform ScaleX="1" ScaleY="-1" CenterX=".5" CenterY=".5"/>
</Canvas.LayoutTransform>
</Canvas>
</ItemsPanelTemplate>
</ItemsControl.ItemsPanel>
<ItemsControl.ItemsSource>
<CompositeCollection>
<CollectionContainer
Collection="{Binding
Source={StaticResource UserControlBindingProxy},
Path=Data.GraphPens,
Mode=OneWay}"/>
<Line X1="10" Y1="0" X2="200" Y2="180" Stroke="DarkRed" StrokeThickness="2"/>
</CompositeCollection>
</ItemsControl.ItemsSource>
</ItemsControl>
<TextBox
x:Name="debug"
Grid.Column="0" Grid.Row="1"
Text="{Binding
Source={StaticResource UserControlBindingProxy},
Path=Data.DebugText,
Mode=OneWay}"/>
</Grid>
</UserControl>
UserControl1.xaml.cs
namespace WpfExampleControlLibrary
{
/// <summary>
/// Interaction logic for UserControl1.xaml
/// </summary>
public partial class UserControl1 : UserControl
{
#region Constructor
public UserControl1()
{
InitializeComponent();
GraphPens = new ObservableCollection<GraphPen>();
}
#endregion Constructor
#region Public Methods
#endregion Public Methods
#region Dependency Properties
// Pens
public static PropertyMetadata GraphPenMetadata = new PropertyMetadata(null);
public static DependencyProperty GraphPensProperty
= DependencyProperty.Register(
"GraphPens",
typeof(ObservableCollection<GraphPen>),
typeof(UserControl1),
GraphPenMetadata);
public ObservableCollection<GraphPen> GraphPens
{
get { return (ObservableCollection<GraphPen>)GetValue(GraphPensProperty); }
set { SetValue(GraphPensProperty, value); }
}
// Debug Text
public static PropertyMetadata DebugTextMetadata = new PropertyMetadata(null);
public static DependencyProperty DebugTextProperty
= DependencyProperty.Register(
"DebugText",
typeof(string),
typeof(UserControl1),
DebugTextMetadata);
public string DebugText
{
get { return (string)GetValue(DebugTextProperty); }
set { SetValue(DebugTextProperty, value); }
}
#endregion Dependency Properties
}
}
GraphPen.cs
namespace WpfExampleControlLibrary
{
public class GraphPen : DependencyObject
{
#region Constructor
public GraphPen()
{
PenGeometry = new PathGeometry();
}
#endregion Constructor
#region Dependency Properties
// Line Color
public static PropertyMetadata PenLineColorPropertyMetadata
= new PropertyMetadata(null);
public static DependencyProperty PenLineColorProperty
= DependencyProperty.Register(
"PenLineColor",
typeof(Brush),
typeof(GraphPen),
PenLineColorPropertyMetadata);
public Brush PenLineColor
{
get { return (Brush)GetValue(PenLineColorProperty); }
set { SetValue(PenLineColorProperty, value); }
}
// Line Thickness
public static PropertyMetadata PenLineThicknessPropertyMetadata
= new PropertyMetadata(null);
public static DependencyProperty PenLineThicknessProperty
= DependencyProperty.Register(
"PenLineThickness",
typeof(Int32),
typeof(GraphPen),
PenLineThicknessPropertyMetadata);
public Int32 PenLineThickness
{
get { return (Int32)GetValue(PenLineThicknessProperty); }
set { SetValue(PenLineThicknessProperty, value); }
}
// Pen Geometry
public static PropertyMetadata PenGeometryMetadata = new PropertyMetadata(null);
public static DependencyProperty PenGeometryProperty
= DependencyProperty.Register(
"PenGeometry",
typeof(PathGeometry),
typeof(GraphPen),
PenGeometryMetadata);
public PathGeometry PenGeometry
{
get { return (PathGeometry)GetValue(PenGeometryProperty); }
set { SetValue(PenGeometryProperty, value); }
}
#endregion Dependency Properties
}
}
BindingProxy.cs
namespace WpfExampleControlLibrary
{
public class BindingProxy : Freezable
{
#region Override Freezable Abstract Parts
protected override Freezable CreateInstanceCore()
{
return new BindingProxy();
}
#endregion Override Freezable Abstract Parts
#region Dependency Properties
// Using a DependencyProperty as the backing store for Data.
// This enables animation, styling, binding, etc...
public static PropertyMetadata DataMetadata = new PropertyMetadata(null);
public static readonly DependencyProperty DataProperty
= DependencyProperty.Register(
"Data",
typeof(object),
typeof(BindingProxy),
DataMetadata);
public object Data
{
get { return (object)GetValue(DataProperty); }
set { SetValue(DataProperty, value); }
}
#endregion Dependency Properties
}
}
MainWindow.xaml
<Window x:Class="POC_WPF_UserControlExample.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:Exmpl="clr-namespace:WpfExampleControlLibrary;assembly=WpfExampleControlLibrary"
xmlns:local="clr-namespace:POC_WPF_UserControlExample"
mc:Ignorable="d"
Title="MainWindow" Height="550" Width="550">
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="100"/>
<ColumnDefinition />
<ColumnDefinition Width="100"/>
</Grid.ColumnDefinitions>
<Grid.RowDefinitions>
<RowDefinition Height="100" />
<RowDefinition />
<RowDefinition Height="100" />
</Grid.RowDefinitions>
<Exmpl:UserControl1 Grid.Column="1" Grid.Row="1" x:Name="myExample"/>
</Grid>
</Window>
MainWindow.xaml.cs
namespace POC_WPF_UserControlExample
{
/// <summary>
/// Interaction logic for MainWindow.xaml
/// </summary>
public partial class MainWindow : Window
{
private DispatcherTimer _timer = null;
private GraphPen _graphPen0 = null;
private Int32 _pos = 0;
private PathFigure _pathFigure0 = null;
private bool _firstTime = true;
public MainWindow()
{
InitializeComponent();
_pathFigure0 = new PathFigure();
_graphPen0 = new GraphPen();
_graphPen0.PenLineColor = Brushes.DarkGoldenrod;
_graphPen0.PenLineThickness = 2;
_graphPen0.PenGeometry = new PathGeometry();
_graphPen0.PenGeometry.Figures.Add(_pathFigure0);
myExample.GraphPens.Add(_graphPen0);
_timer = new DispatcherTimer();
_timer.Tick += Timer_Tick;
_timer.Interval = new TimeSpan(0, 0, 0, 0, 100);
_timer.Start();
}
private void Timer_Tick(object sender, EventArgs e)
{
_pos++;
Point penPoint0 = new Point(_pos, _pos + 20);
if (_firstTime)
{
myExample.GraphPens[0].PenGeometry.Figures[0].StartPoint = penPoint0;
_firstTime = false;
}
else
{
LineSegment segment = new LineSegment(penPoint0, true);
myExample.GraphPens[0].PenGeometry.Figures[0].Segments.Add(segment);
}
myExample.DebugText = _pos.ToString();
}
}
}