I am extending ComboBox control to include a label in front, via dependency property as per below:
ComboBoxEx.xaml.cs
public partial class ComboBoxEx : UserControl
{
public ComboBoxEx()
{
InitializeComponent();
}
public static readonly DependencyProperty LabelProperty = DependencyProperty.Register(
nameof(Label), typeof(string), typeof(ComboBoxEx), new PropertyMetadata(default(string)));
public string Label
{
get { return (string)GetValue(LabelProperty); }
set { SetValue(LabelProperty, value); }
}
public static readonly DependencyProperty SelectedItemProperty = DependencyProperty.Register(
nameof(SelectedItem), typeof(object), typeof(ComboBoxEx), new PropertyMetadata(default(object)), SelectedItemChanged);
private static bool SelectedItemChanged(object value)
{
return true;
}
public object SelectedItem
{
get { return (object)GetValue(SelectedItemProperty); }
set { SetValue(SelectedItemProperty, value); }
}
public static readonly DependencyProperty ItemsSourceProperty = DependencyProperty.Register(
nameof(ItemsSource), typeof(IEnumerable), typeof(ComboBoxEx), new PropertyMetadata(default(IEnumerable)));
public IEnumerable ItemsSource
{
get { return (IEnumerable)GetValue(ItemsSourceProperty); }
set { SetValue(ItemsSourceProperty, value); }
}
public static readonly DependencyProperty DisplayMemberPathProperty = DependencyProperty.Register(
nameof(DisplayMemberPath), typeof(string), typeof(ComboBoxEx), new PropertyMetadata(default(string)));
public string DisplayMemberPath
{
get { return (string)GetValue(DisplayMemberPathProperty); }
set { SetValue(DisplayMemberPathProperty, value); }
}
}
ComboBoxEx.xaml
<UserControl x:Class="LabelDoubleTextBox.ComboBoxEx"
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:LabelDoubleTextBox"
mc:Ignorable="d"
d:DesignHeight="450" d:DesignWidth="800">
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto" SharedSizeGroup="LabelGroup"/>
<ColumnDefinition Width="Auto" SharedSizeGroup="ValueGroup"/>
</Grid.ColumnDefinitions>
<Label Grid.Column="0" Content="{Binding Label,RelativeSource={RelativeSource FindAncestor, AncestorType={x:Type local:ComboBoxEx}}}" Margin="5"/>
<ComboBox Grid.Column="1" ItemsSource="{Binding ItemsSource,RelativeSource={RelativeSource FindAncestor, AncestorType={x:Type local:ComboBoxEx}}}"
SelectedItem="{Binding SelectedItem,RelativeSource={RelativeSource FindAncestor, AncestorType={x:Type local:ComboBoxEx}}}"
DisplayMemberPath="{Binding DisplayMemberPath,RelativeSource={RelativeSource FindAncestor, AncestorType={x:Type local:ComboBoxEx}}}"
Margin="5" />
</Grid>
</UserControl>
Here's my ViewModels:
public class MainWindowVM:INotifyPropertyChanged
{
private Person _selectedHead;
public string VMValue => "This is a value";
public string Terrify => "Dont be terrify";
public List<Person> Persons { get; }
public Person SelectedHead
{
get => _selectedHead;
set
{
_selectedHead = value;
OnPropertyChanged(nameof(SelectedHead));
}
}
public MainWindowVM()
{
Persons = new List<Person>();
Persons.Add(new Person("Allen", "A Photographer"));
Persons.Add(new Person("Steven", "A Tycoon"));
SelectedHead = Persons.First();
}
public event PropertyChangedEventHandler? PropertyChanged;
protected virtual void OnPropertyChanged([CallerMemberName] string? propertyName = null)
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
protected bool SetField<T>(ref T field, T value, [CallerMemberName] string? propertyName = null)
{
if (EqualityComparer<T>.Default.Equals(field, value)) return false;
field = value;
OnPropertyChanged(propertyName);
return true;
}
}
public class Person
{
public string Name { get; set; }
public string Description { get; set; }
public Person(string name, string description)
{
Name = name;
Description = description;
}
}
And my MainWindow.xaml:
<Window x:Class="LabelDoubleTextBox.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:LabelDoubleTextBox"
mc:Ignorable="d"
Title="MainWindow" Height="450" Width="800">
<Window.DataContext>
<local:MainWindowVM/>
</Window.DataContext>
<Grid IsSharedSizeScope="True">
<Grid.RowDefinitions>
<RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/>
</Grid.RowDefinitions>
<local:ComboBoxEx Grid.Row="2" Label="5A99 perfect" ItemsSource="{Binding Persons}" SelectedItem="{Binding SelectedHead}" DisplayMemberPath="Name"/>
</Grid>
</Window>
I put a breakpoint at SelectedItemChanged
method in ComboBoxEx
, and a breakpoint in _selectedHead = value;
in MainWindowVM
.
When I toggle the ComboBox, I expect that both the breakpoint will be hit, and that the SelectedItem is properly set ( as it seems properly bind). Alas, only the breakpoint in SelectedItemChanged
method is being hit, the one _selectedHead = value;
in MainWindowVM
is not.
So it seems that the change in the UI is never being communicated to the ViewModel? Why?
The Binding
SelectedItem="{Binding SelectedHead}"
is by default OneWay. Either make it TwoWay, or declare the SelectedItem
property with the metadata option BindsTwoWayByDefault
:
public static readonly DependencyProperty SelectedItemProperty =
DependencyProperty.Register(
nameof(SelectedItem), typeof(object), typeof(ComboBoxEx),
new FrameworkPropertyMetadata(null,
FrameworkPropertyMetadataOptions.BindsTwoWayByDefault));
public object SelectedItem
{
get => GetValue(SelectedItemProperty);
set => SetValue(SelectedItemProperty, value);
}