WPF custom control: Proper way to react to property changes [duplicate] - wpf

This question already has answers here:
DependencyProperty not triggered
(2 answers)
Callback when dependency property recieves xaml change
(2 answers)
Closed 1 year ago.
I have created a custom control in WPF: ChannelsDisplay, a kind of very simple charts control. For testing the control I use a MVVM test application:
View
<UserControl x:Class="UI.CustomControls.TestApp.Views.ChannelsDisplayView"
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:UI.CustomControls.TestApp.Views"
xmlns:ucc="clr-namespace:UI.CustomControls;assembly=UI.CustomControls"
xmlns:viewmodels="clr-namespace:UI.CustomControls.TestApp.ViewModels"
d:DataContext="{d:DesignInstance Type=viewmodels:ChannelsDisplayViewModel}"
mc:Ignorable="d"
d:DesignHeight="450" d:DesignWidth="800">
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="*"/>
<RowDefinition Height="Auto"/>
</Grid.RowDefinitions>
<ucc:ChannelsDisplay Lines="{Binding Lines}"
ChannelsYSpacing="{Binding YSpacing, Mode=TwoWay}"/>
<StackPanel Grid.Row="1" Orientation="Vertical">
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto"/>
<ColumnDefinition Width="*"/>
<ColumnDefinition Width="Auto"/>
</Grid.ColumnDefinitions>
<Label Content="ChannelsYSpacing" />
<Slider Grid.Column="1"
Value="{Binding YSpacing, Mode=TwoWay}"
Minimum="0" Maximum="99" IsSnapToTickEnabled="True"/>
<TextBox Grid.Column="2" Text="{Binding YSpacing}"/>
</Grid>
</StackPanel>
</Grid>
</UserControl>
ViewModel
public class ChannelsDisplayViewModel : ObservableObject, IPageViewModel
{
public string Name => "ChannelsDisplay";
public ObservableCollection<List<int>> Lines { get; set; }
public int YSpacing
{
get { return ySpacing; }
set
{
if (value != ySpacing)
{
loginViewModel = value;
OnPropertyChanged(nameof(YSpacing));
}
}
}
// ...
};
In the view there is a Slider, a TextBox and the ChannelsDisplay, all of them bind to the view model property YSpacing. The aim is that the Slider can change the YSpacing of my ChannelsDisplay.
So the ChannelsDisplay has among other things a property ChannelsYSpacing, which is implemented as a full DependencyProperty:
/// <summary>
/// Y-Distance between each band of data
/// </summary>
[Category("Common")]
public int ChannelsYSpacing
{
get => (int)GetValue(ChannelsYSpacingProperty);
set
{
SetValue(ChannelsYSpacingProperty, value);
createScaledLines();
createVisibleLines();
updateCanvas();
}
}
/// <summary>
/// Identifies the <see cref="Lines"/> dependency property.
/// </summary>
public static readonly DependencyProperty ChannelsYSpacingProperty =
DependencyProperty.Register("ChannelsYSpacing",
typeof(int),
typeof(ChannelsDisplay),
new FrameworkPropertyMetadata(20, new PropertyChangedCallback(OnSpacingChanged)));
The problem is, if the bound value of ChannelsYSpacing changes (E.g. the slider was moved by the user) the ChannelsDisplay custom control must be updated and redrawn. And the only way to do it that I could find out to write some code into the PropertyChangedCallback:
private static void OnSpacingChanged(DependencyObject d,
DependencyPropertyChangedEventArgs e)
{
ChannelsDisplay channelsDisplay = (ChannelsDisplay)d;
if(e.Property.Name == nameof(ChannelsYSpacing))
{
channelsDisplay.ChannelsYSpacing = (int)e.NewValue;
}
}
So the custom controls property ChannelsYSpacing will be set to e.NewValue and the property setter then calls createScaledLines(), createVisibleLines() and updateCanvas().
That works, the control is updated when the slider is moved. But going through the callback handler and writing things like if (e.Property.Name == nameof(ChannelsYSpacing)) somehow feels not right. It feelslike overhead. Ist this really necessary or is there a better way to do it?

Related

How to bind a UserControl's property to a property?

I'd like to set a property of a re-defined UserControl (for example its background color) to a property of the class. For example.
If I define the background of a Button to a property (<Button x:Name="myButton" Background="{Binding ColorName}"/>), it works fine. However, if I do the same for a re-defined UserControl (<local:MyUserControl Background="{Binding Path=ColorName}"/>), it does not.
What's funny though, is that, if I do <local:MyUserControl Background="{Binding Background, ElementName=myButton}"/>, it works perfectly fine.
Could I have some help on that? I must be missing something.
Thanks!
EDIT
Here is all the code. The setting of the background color worked fine. What solved this was to set properly the MainWindow.DataContext and to remove the DataContext = this in MyUserControl.xaml.cs. Setting Color as a DependencyProperty is also useful to be able to change the Color setting in a later execution of the code.
Nonetheless, while removing DataContext=this in MyUserControl.xaml.cs,
the {Binding TextContent} does not work and needs to be replaced by {Binding TextContent, RelativeSource={RelativeSource AncestorType=c:MyUserControl}}.
MainWindow.xaml
<Window x:Class="BindingBug.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:c="clr-namespace:BindingBug"
Title="MainWindow" Height="350" Width="525"
DataContext="{Binding RelativeSource={RelativeSource Self}}">
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="1*"/>
<RowDefinition Height="1*"/>
<RowDefinition Height="1*"/>
</Grid.RowDefinitions>
<Button Background="{Binding Path=Color}"
Width="250"
Height="30"
Content="I am bound to be RED!"
Grid.Row="0"
x:Name="myButton"/>
<c:MyUserControl Background="{Binding Background, ElementName=myButton}"
Width="250"
Height="30"
Content="I am bound to be RED!"
Grid.Row="1"/>
<c:MyUserControl Background="{Binding Path=Color}"
Width="250"
Height="30"
Content="I am bound to be RED!"
Grid.Row="2"/>
</Grid>
</Window>
MainWindow.xaml.cs
using System.Windows;
using System.Windows.Media;
namespace BindingBug
{
/// <summary>
/// Interaction logic for MainWindow.xaml
/// </summary>
public partial class MainWindow : Window
{
public MainWindow()
{
InitializeComponent();
Color = Brushes.Red;
}
public static readonly DependencyProperty ColorProperty = DependencyProperty.Register("Color", typeof(Brush), typeof(MainWindow));
public Brush Color
{
get
{
return (Brush)GetValue(ColorProperty);
}
set
{
SetValue(ColorProperty, value);
}
}
}
}
MyUserControl.xaml
<UserControl x:Class="BindingBug.MyUserControl"
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:c="clr-namespace:BindingBug"
mc:Ignorable="d"
d:DesignHeight="300" d:DesignWidth="300">
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="1*" />
<RowDefinition Height="2*" />
</Grid.RowDefinitions>
<TextBlock Grid.Row="0"
FontSize="13"
Text="{Binding TextContent, RelativeSource={RelativeSource AncestorType=c:MyUserControl}}"
VerticalAlignment="Center"/>
</Grid>
</UserControl>
MyUserControl.xaml.cs
using System.Windows;
using System.Windows.Controls;
namespace BindingBug
{
/// <summary>
/// Interaction logic for NumberDataHolder.xaml
/// </summary>
public partial class MyUserControl : UserControl
{
public MyUserControl()
{
InitializeComponent();
}
public static readonly DependencyProperty TextContentProperty = DependencyProperty.Register("TextContent", typeof(string), typeof(MyUserControl));
public string TextContent
{
get
{
return (string)GetValue(TextContentProperty);
}
set
{
SetValue(TextContentProperty, value);
}
}
}
}
EDIT 2
I tried to acheive the same results without having to declare the whole Text="{Binding TextContent, RelativeSource={RelativeSource AncestorType=c:MyUserControl}}" inside TextBlock. So, following #KeithStein advice, I placed DataContext="{Binding RelativeSource={RelativeSource Self}}" inside MyUserControl and only kept Text="{Binding TextContent}"inside TextBlock. That, however cancels the effect of setting Background="{Binding Path=Color}" in MainWindow.xaml. Any idea why? Is there another possibility to set Background="{Binding Path=Color}" in MainWindow.xaml and to only keepText="{Binding TextContent}"inside TextBlock?
MainWindow.xaml
<Window x:Class="BindingBug.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:c="clr-namespace:BindingBug"
Title="MainWindow" Height="350" Width="525"
DataContext="{Binding RelativeSource={RelativeSource Self}}">
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="1*"/>
<RowDefinition Height="1*"/>
<RowDefinition Height="1*"/>
</Grid.RowDefinitions>
<Button Background="{Binding Path=Color}"
Width="250"
Height="30"
Content="I am bound to be RED!"
Grid.Row="0"
x:Name="myButton"/>
<c:MyUserControl Background="{Binding Background, ElementName=myButton}"
Width="250"
Height="30"
Content="I am bound to be RED!"
Grid.Row="1"/>
<c:MyUserControl Background="{Binding Path=Color}"
Width="250"
Height="30"
Content="I am bound to be RED!"
Grid.Row="2"/>
</Grid>
</Window>
MainWindow.xaml.cs
using System.Windows;
using System.Windows.Media;
namespace BindingBug
{
/// <summary>
/// Interaction logic for MainWindow.xaml
/// </summary>
public partial class MainWindow : Window
{
public MainWindow()
{
InitializeComponent();
Color = Brushes.Red;
}
public static readonly DependencyProperty ColorProperty = DependencyProperty.Register("Color", typeof(Brush), typeof(MainWindow));
public Brush Color
{
get
{
return (Brush)GetValue(ColorProperty);
}
set
{
SetValue(ColorProperty, value);
}
}
}
}
MyUserControl.xaml
<UserControl x:Class="BindingBug.MyUserControl"
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:c="clr-namespace:BindingBug"
mc:Ignorable="d"
d:DesignHeight="300" d:DesignWidth="300"
DataContext="{Binding RelativeSource={RelativeSource Self}}">
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="1*" />
<RowDefinition Height="2*" />
</Grid.RowDefinitions>
<TextBlock Grid.Row="0"
FontSize="13"
Text="{Binding TextContent}"
VerticalAlignment="Center"/>
</Grid>
</UserControl>
MyUserControl.xaml.cs
using System.Windows;
using System.Windows.Controls;
namespace BindingBug
{
/// <summary>
/// Interaction logic for NumberDataHolder.xaml
/// </summary>
public partial class MyUserControl : UserControl
{
public MyUserControl()
{
InitializeComponent();
}
public static readonly DependencyProperty TextContentProperty = DependencyProperty.Register("TextContent", typeof(string), typeof(MyUserControl));
public string TextContent
{
get
{
return (string)GetValue(TextContentProperty);
}
set
{
SetValue(TextContentProperty, value);
}
}
}
}
This answer developed gradually through back and forth comments with OP. To summarize:
Use a Brush-type dependency property for your color. Brush because that is the type of the Background property that you want to bind to, and a dependency property so that updates of the property trigger any bindings to refresh.
When binding inside a Window or UserControl, you need to set DataContext, which is essentially the default sourced used by bindings.
For a Window, add DataContext="{Binding RelativeSource={RelativeSource Self}}" to the opening tag. This sets the default source for all controls contained within to the Window itself.
For a UserControl, add the following to the outer-most panel of said control: DataContext={Binding RelativeSource={RelativeSource AncestorType=UserControl}} (UserControl can be replaced with the name of your particular control, i.e. c:MyUserControl). This tells everything inside that root panel to use the UserControl as the default source. You can't use RelativeSource Self in this case, because then instances of the MyUserControl will bind to themselves when placed inside Windows, instead of inheriting the Window's DataContext.

Binding text property of text box to variable defined on MainWindow-WPF

I'm newbie on WPF and I have text box and button which open folder browser dialog.
When the user select folder I would like text box will contain the selected path.
So on MainWindow I added two variables:
public partial class MainWindow : Window
{
public string outputFolderPath { get; set; }
string reducedModelFolderPath { get; set; }
}
and when user selected folder path (after open folder dialog) I updated those variables by doing (for example):
outputFolderPath = dialog.SelectedPath
In MainWindow.xaml:
<TextBox x:Name="outputFolder" Width ="200" Height="30" Grid.Row="1" Grid.Column="1" Margin="5 10">
How can I bind TextBox.Text to outputFolderPath variable?
Thanks for your assitance!
You need to set DataContext of your window to this, to access your property in XAML, and after that bind to the property. As you are binding not to DependencyProperty, you should notify your binding that property has changed, which could be done by implementing INotifyPropertyChanged interface in your Window.
I've provided sample code to show the concept.
But this is very ugly, much better to use MVVM pattern instead.
MainWindow.xaml.cs
public partial class MainWindow : Window, INotifyPropertyChanged
{
public string outputFolderPath { get; set; }
public MainWindow()
{
InitializeComponent();
DataContext = this;
}
public event PropertyChangedEventHandler PropertyChanged;
private void Button_Click(object sender, RoutedEventArgs e)
{
outputFolderPath = "Some data";
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(outputFolderPath)));
}
}
MainWindow.xaml
<Window x:Class="simplest.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:local="clr-namespace:simplest"
mc:Ignorable="d"
Title="MainWindow" Height="350" Width="525">
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition/>
<ColumnDefinition/>
</Grid.ColumnDefinitions>
<Grid.RowDefinitions>
<RowDefinition />
<RowDefinition />
</Grid.RowDefinitions>
<Button Click="Button_Click" Content="Go" />
<TextBox x:Name="outputFolder" Width ="200" Height="30" Grid.Row="1" Grid.Column="1" Margin="5 10" Text="{Binding outputFolderPath}"/>
</Grid>
</Window>

DoubleUpDown of Extended WPF Toolkit, what am I doing wrong?

Ok, I thought this would be a no-brainer, but evidently I'm doing something wrong. The problem is that when clicking on the "Up" and "Down" buttons of the Extended WPF toolkit DoubleUpDown control, the values do not get updated correctly. When I click Up, the value in the control changes, but the view model does not get updated. Only when I change from clicking Up to clicking Down, does the model get updated, but with the then previous value.
To reproduce, I used a simple view model like so:
public class ViewModel : INotifyPropertyChanged
{
public ViewModel()
{
MyValue = 0.5;
}
private double _myValue;
public double MyValue
{
get { return _myValue; }
set
{
_myValue = value;
PropertyChanged(this, new PropertyChangedEventArgs("MyValue"));
}
}
public event PropertyChangedEventHandler PropertyChanged = delegate { };
}
And my MainWindow.xaml looks like the code below, where the DoubleUpDown control and the label are both bound in TwoWay fashion to the ViewModel's MyValue property:
<Window x:Class="WpfApplication1.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:xctk="http://schemas.xceed.com/wpf/xaml/toolkit"
Title="MainWindow" Height="100" Width="200">
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*"/>
<ColumnDefinition Width="*"/>
</Grid.ColumnDefinitions>
<xctk:DoubleUpDown
Value="{Binding MyValue, Mode=TwoWay}"
Increment="0.5"
Minimum="0.0"
Maximum="10"
ValueChanged="DoubleUpDown_ValueChanged"
/>
<Label Grid.Column="1" Content="{Binding MyValue, Mode=TwoWay}"/>
</Grid>
</Window>
And in the code-behind, I set the DataContext in the MainWindow constructor to be an instance of ViewModel:
public MainWindow()
{
DataContext = new ViewModel();
InitializeComponent();
}
Default binding update logic for DoubleUpDown control is LostFocus. Try setting explicitly UpdateSourceTrigger=PropertyChanged in your binding like this -
<xctk:DoubleUpDown
Value="{Binding MyValue, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}"
Increment="0.5"
Minimum="0.0"
Maximum="10"
ValueChanged="DoubleUpDown_ValueChanged"/>

Set User Control Dependency Property value to a lable control

I am creating a user control, inside the control I have a Label Control. (for example: an email control, contains a label and a text box).
In the email address control, I defined a Title property, which when the user changes the property value, I want to set the string value to the Label.
bellow is the XAML and Code:
XAML:
<UserControl x:Class="SIMind.ClinicManagement.GUI.Controls.CommonControl.EmailAddressCtrl"
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" Height="32" MinHeight="32" MaxHeight="32" Width="386" MinWidth="386">
<Grid>
<Grid.RowDefinitions>
<RowDefinition />
</Grid.RowDefinitions>
<StackPanel HorizontalAlignment="Stretch" Margin="0" Name="stackPanel1" VerticalAlignment="Stretch" Orientation="Horizontal">
<Label Content="Email :" Height="24" HorizontalContentAlignment="Right" Name="lblEmailCaption" Tag="PhoneType" Width="140" />
<TextBox MaxLength="100" Name="txtEmail" Text="email#server.com" Width="240" Margin="1" Height="23" />
</StackPanel>
</Grid>
</UserControl>
Code:
namespace SIMind.ClinicManagement.GUI.Controls.CommonControl
{
/// <summary>
/// Interaction logic for EmailAddressCtrl.xaml
/// </summary>
public partial class EmailAddressCtrl : UserControl
{
public EmailAddressCtrl()
{
InitializeComponent();
}
#region Dependency Property
#region Title Property
/// <summary>
/// Title property
/// </summary>
public static readonly DependencyProperty TitleProperty =
DependencyProperty.Register("Title", typeof(String),
typeof(EmailAddressCtrl), new PropertyMetadata("Phone Number :"));
/// <summary>
/// Gets and Sets the main label of the control.
/// </summary>
public string Title
{
get { return (string)GetValue(TitleProperty); }
set {
SetValue(TitleProperty, value);
lblEmailCaption.Content = value;
}
}
#endregion
#endregion
}
}
But it seems not working the way I wanted it to be: The dependency property is set, but the label is not refreshed to be the property set.
Any one got a good answer? :-)
Hope this will work for you
<Label Content="{Binding RelativeSource={RelativeSource FindAncestor, AncestorType={x:Type EmailAddressCtrl}},Path=Title}" Height="24" HorizontalContentAlignment="Right" Name="lblEmailCaption" Tag="PhoneType" Width="140" />

Interesting Issue with Silverlight Datagrid

Folks,
I'm having an interesting issue with Silverlight DataGrid data binding. It may be b/c I'm not binding the data source properly. Here's the object & the observable collection
/// <summary>
/// Interface for all model elements
/// </summary>
public interface IBaseModel
{
}
/// <summary>
/// Employee model
/// </summary>
public class EmployeeModel : IBaseModel
{
public string FirstName { get; set; }
public string LastName { get; set; }
public override string ToString()
{
return FirstName + LastName;
}
}
// The observable collection is loaded and bound in the user control
public partial class EmployeeMasterDetailsWindow : UserControl
{
public EmployeeMasterDetailsWindow()
{
try
{
InitializeComponent();
ObservableCollection<IBaseModel> k = new ObservableCollection<IBaseModel>()
{new EmployeeModel(){FirstName="Frodo",
LastName=" Baggins"},
new EmployeeModel(){FirstName="Pippin",
LastName="Thomas"},
new EmployeeModel(){FirstName="John",
LastName="Doe"},
new EmployeeModel(){FirstName="Tim",
LastName="Kiriev"}};
dataGrid1.DataContext = k;
CustomersListBox.DataContext = k;
}
catch (Exception ex)
{
}
}
}
//here's the XAML
<UserControl x:Class="AdventureWorksManagement.UI.EmployeeMasterDetailsWindow"
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"
mc:Ignorable="d"
d:DesignHeight="379" d:DesignWidth="516"
xmlns:sdk="http://schemas.microsoft.com/winfx/2006/xaml/presentation/sdk"
xmlns:toolkit="http://schemas.microsoft.com/winfx/2006/xaml/presentation/toolkit">
<UserControl.Resources>
<DataTemplate x:Key="CustomerTemplate">
<StackPanel Orientation="Horizontal">
<TextBlock Text="{Binding FirstName}" />
<TextBlock Text=" " />
<TextBlock Text="{Binding LastName}" />
</StackPanel>
</DataTemplate>
</UserControl.Resources>
<Grid x:Name="LayoutRoot" Background="White" Height="371" Width="595">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="312*" />
<ColumnDefinition Width="283*" />
</Grid.ColumnDefinitions>
<sdk:DataGrid Height="325" HorizontalAlignment="Left"
Margin="12,12,0,0" Name="dataGrid1" VerticalAlignment="Top" Width="271" ItemsSource="{Binding}"
RowDetailsTemplate="{StaticResource CustomerTemplate}">
</sdk:DataGrid>
<ListBox x:Name="CustomersListBox"
Margin="10,10,10,11"
ItemsSource="{Binding}"
ItemTemplate="{StaticResource CustomerTemplate}" />
</Grid>
The Listbox shows all the of the employees, but the DataGrid doesn't. I don't even see the DataGrid. I see this error message in the output window:
'System.Collections.ObjectModel.ObservableCollection1[AdventureWorksManagement.Model.IBaseModel]'
'System.Collections.ObjectModel.ObservableCollection1[AdventureWorksManagement.Model.IBaseModel]'
(HashCode=54025633).
BindingExpression: Path='FirstName'
DataItem='System.Collections.ObjectModel.ObservableCollection`1[AdventureWorksManagement.Model.IBaseModel]'
(HashCode=54025633); target element is
'System.Windows.Controls.TextBlock'
(Name=''); target property is 'Text'
(type 'System.String')..
What could I be doing wrong?
By making it an ObservableCollection<IBaseModel> you are effectively casting all the child objects to IBaseModel, which has no members.
In this instance make it an ObservableCollection<EmployeeModel>.

Resources