If someone can help me before I go crazy.
I have a User Control who contains a ListBox
I would like to add a property for the SelectedItem to the UserControl, so the parent can get it.
So I used a DependencyProperty
UserControl (VersionList.xaml):
<UserControl
x:Class="PcVueLauncher.Controls.VersionsList"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:converters="clr-namespace:PcVueLauncher.Converters"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:local="clr-namespace:PcVueLauncher.Controls"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
d:Background="white"
d:DesignHeight="450"
d:DesignWidth="800"
mc:Ignorable="d">
<FrameworkElement.Resources>
<ResourceDictionary>
<converters:BoolToVisibilityConverter x:Key="BoolToVisibilityConverter" />
</ResourceDictionary>
</FrameworkElement.Resources>
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
<RowDefinition Height="*" />
<RowDefinition Height="Auto" />
</Grid.RowDefinitions>
<TextBlock
Grid.Row="0"
Padding="10"
Text="Versions" />
<ListBox
Grid.Row="1"
d:ItemsSource="{d:SampleData ItemCount=5}"
ItemsSource="{Binding Versions}"
SelectedItem="{Binding SelectedVersion}">
<ListBox.ItemTemplate>
<DataTemplate>
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*" />
<ColumnDefinition Width="auto" />
</Grid.ColumnDefinitions>
<TextBlock
Grid.Column="0"
Margin="5,5,10,5"
Text="{Binding VersionName}" />
<Button
Grid.Column="1"
Padding="5"
Command="{Binding RemoveVersionCommand}"
Content="Remove"
Visibility="{Binding CanBeRemoved, Converter={StaticResource BoolToVisibilityConverter}}" />
</Grid>
</DataTemplate>
</ListBox.ItemTemplate>
</ListBox>
</Grid>
</UserControl>
UserControl Associated ViewModel (VersionListViewModel)
namespace PcVueLauncher.ViewModels.Controls
{
public class VersionsListViewModel : ViewModelBase
{
private List<VersionPcVue> _versions;
public List<VersionPcVue> Versions
{
get
{
return _versions;
}
set
{
_versions = value;
OnPropertyChanged(nameof(Versions));
}
}
private VersionPcVue _selectedVersion;
public VersionPcVue SelectedVersion
{
get
{
return _selectedVersion;
}
set
{
_selectedVersion = value;
OnPropertyChanged(nameof(SelectedVersion));
}
}
public ICommand RemoveVersionCommand { get; }
public VersionsListViewModel()
{
List<VersionPcVue> versionPcVues = new()
{
new VersionPcVue{VersionName="V15"},
new VersionPcVue{VersionName="V12"}
};
Versions = versionPcVues;
}
}
}
UserControl code behind (VersionList.cs):
public partial class VersionsList : UserControl
{
public VersionsList()
{
InitializeComponent();
}
public VersionPcVue SelectedVersion
{
get { return (VersionPcVue)GetValue(SelectedVersionProperty); }
set { SetValue(SelectedVersionProperty, value); }
}
//Using a DependencyProperty as the backing store for SelectedVersion.This enables animation, styling, binding, etc...
public static readonly DependencyProperty SelectedVersionProperty =
DependencyProperty.Register("SelectedVersion",
typeof(VersionPcVue),
typeof(VersionsList),
new FrameworkPropertyMetadata(
defaultValue: null,
flags: FrameworkPropertyMetadataOptions.AffectsMeasure,
propertyChangedCallback: new PropertyChangedCallback(OnSelectionChanged)));
private static void OnSelectionChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
d.CoerceValue(SelectedVersionProperty);
}
}
// Register a dependency property with the specified property name,
// property type, owner type, property metadata, and callbacks.
public static readonly DependencyProperty SelectedVersionProperty = DependencyProperty.Register(
name: "SelectedVersion",
propertyType: typeof(VersionPcVue),
ownerType: typeof(VersionsList),
typeMetadata: new FrameworkPropertyMetadata(
defaultValue: null,
flags: FrameworkPropertyMetadataOptions.AffectsMeasure,
propertyChangedCallback: new PropertyChangedCallback(OnSelectionChanged)
));
private static void OnSelectionChanged(DependencyObject depObj, DependencyPropertyChangedEventArgs e)
{
depObj.CoerceValue(SelectedVersionProperty);
}
In the HomeView which contains the UserControl, I have this :
<UserControl
x:Class="PcVueLauncher.Views.HomeView"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:Controls="clr-namespace:PcVueLauncher.Controls"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:local="clr-namespace:PcVueLauncher.Views"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:system="clr-namespace:System;assembly=netstandard"
xmlns:viewmodels="clr-namespace:PcVueLauncher.ViewModels"
d:Background="White"
d:DataContext="{d:DesignInstance Type=viewmodels:HomeViewModel}"
d:DesignHeight="450"
d:DesignWidth="800"
mc:Ignorable="d">
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*" />
<ColumnDefinition Width="*" />
<ColumnDefinition Width="2*" />
</Grid.ColumnDefinitions>
<Controls:VersionsList
x:Name="test"
Grid.Column="0"
DataContext="{Binding VersionsListViewModel}"
SelectedVersion="{Binding DataContext.SelectedVersion, RelativeSource={RelativeSource FindAncestor, AncestorLevel=1, AncestorType={x:Type UserControl}}, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}" />
</Grid>
</UserControl>
And in the associated ViewModel (HomeViewModel)
public class HomeViewModel : ViewModelBase
{
private IProjectService _projectService;
private VersionPcVue _selectedVersion;
public VersionPcVue SelectedVersion
{
get
{
return _selectedVersion;
}
set
{
_selectedVersion = value;
OnPropertyChanged(nameof(SelectedVersion));
}
}
private VersionPcVue _test1;
public VersionPcVue Test1
{
get
{
return _test1;
}
set
{
_test1 = value;
OnPropertyChanged(nameof(Test1));
}
}
private string _test;
public string Test
{
get
{
return _test;
}
set
{
_test = value;
OnPropertyChanged(nameof(Test));
}
}
private VersionsListViewModel versionsListViewModel;
public VersionsListViewModel VersionsListViewModel
{
get
{
return versionsListViewModel;
}
set
{
versionsListViewModel = value;
OnPropertyChanged(nameof(VersionsListViewModel));
}
}
public HomeViewModel(IProjectService projectService)
{
_projectService = projectService;
VersionsListViewModel = new();
}
}
When I change the selected item from my user control, nothing happens in the HomeViewModel.
I thought about a binding error, but to try, I changed this
SelectedVersion="{Binding DataContext.SelectedVersionnnnnnn, RelativeSource={RelativeSource FindAncestor, AncestorLevel=1, AncestorType={x:Type Grid}}, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}" />
And Visual Studio tells me that the SelectedVersionnnnn does not exist in HomeViewModel.
Why can’t I get back the Selected Item back to the SelectedVersion property of my HomeViewModel.
Thanks a lot for your help
2
Answers
In
VersionList.xaml
:This only bind
ListBox.SelectedItem
to{DataContext}.SelectedVersion
. Then when a item is selected, the dependency propertyVersionList.SelectedVersion
isn’t updated.Solution 1 : By view model (without dependency property)
I think you mixed-up because you try to use a dependency property that is complex. A easy way is to use directly the view model without dependency property.
In
VersionsList.cs
, removeSelectedVersionProperty
andSelectedVersion
members.Keep
VersionList.xaml
with :So
ListBox.SelectedItem
is bind toListBox.DataContext.SelectedVersion
. IfListBox.DataContext
isVersionsListViewModel
, thenListBox.SelectedItem
is bind toVersionsListViewModel.SelectedVersion
.In the parent controls
HomeView
, it only need to pass aVersionsListViewModel
to theVersionList.DataContext
:So
HomeView.VersionsList.ListBox.SelectedItem
is bindHomeView.DataContext.VersionsListViewModel.SelectedVersion
. IfHomeView.DataContext
isHomeViewModel
, thenHomeView.VersionsList.ListBox.SelectedItem
is bindHomeViewModel.VersionsListViewModel.SelectedVersion
.Finally, you can remove the member
HomeViewModel.SelectedVersion
and useHomeViewModel.VersionsListViewModel.SelectedVersion
.If you want keep the member
HomeViewModel.SelectedVersion
, then you need to redirectHomeViewModel.SelectedVersion
toHomeViewModel.VersionsListViewModel.SelectedVersion
inHomeViewModel.cs
:The trick is when
HomeViewModel.VersionsListViewModel.SelectedVersion
is changed, also notify thatHomeViewModel.SelectedVersion
is changed.Solution 2 : By dependency property
In sumary, you want when a item is selected that set the selected item in
VersionsList.SelectedVersion
, then you just need to bindListBox.SelectedItem
toVersionsList.SelectedVersion
.First, add the dependency property
SelectedVersion
inVersionList.cs
:In
VersionList.xaml
:{RelativeSource TemplatedParent}
indicate the binding refer to the element to which the template, hereVersionsList
.To use the control :
Finally, you can remove the member
VersionsListViewModel.SelectedVersion
(or use the trick below).What to choose?
With dependency property, the control isn’t link to view model class. I will use this to develop a library to reuse in many application.
With view model, the control expect specific members in the data context. I will use this in the application solution.
You need to fix several issues:
Don’t explicitly call
DependencyObject.CoerceValue
from the property changed callback. It is invoked automatically by the dependency property system – before the property changed callback.Your property does not affect the layout. For the sake of a better performance, don’t set the
FrameworkPropertyMetadataOptions.AffectsMeasure
flag as it will force a complete layout pass every time the property changes, which is unnecessary in your case. TheListBox.SelectedItem
has no influence on the layout of your control. Instead you should consider to configure the property to bind two way by default by setting theFrameworkPropertyMetadataOptions.BindsTwoWayByDefault
flag.a) Bind internals of a
Control
to the control’s properties and not to theDataContext
. Otherwise your control becomes inconvenient to handle (and to write). For example, if you change theDataContext
or rename properties on the object returned by theDataContext
, you are forced to rewrite the internal bindings to address the new object structure/property names.b) This means you have to remove all internal
DataContext
bindings and introduce a dependency property for each. For example, remove theListBox.ItemsSource
binding to theVersionsListViewModel.Versions
property and introduce a e.g.,VersionsItemsSource
dependency property instead.For example in
HomeView
: define allDataContext
relatedBinding
relative to the actualDataContext
of theUserControl
(orFrameworkElement
in general) instead of starting a traversal (usingBinding.RelativeSource
) to find the parent’sDataContext
that is the same as the binding target’sDataContext
. It’s pointless and only shows that you have not understood how Binding works.Fixes
1 & 2 & 3b
VersionList.xaml.cs
3a
VersionList.xaml
When authoring a
Control
, always bind to properties of the control instead to properties of theDataContext
:4
HomeView.xaml
Note that the
DataContext
of theVersionsList
control is referencing aVersionsListViewModel
instance. You must adjust all bindings accordingly:Remarks
Given your current class design and control configuration, your
VersionsList
control binds to theVersionsListViewModel.SelectedVersion
property. It’s not clear what you really want at this point.Either delegate the value manually by letting the
HomeViewModel
listen toVersionsListViewModel.SelectedVersion
property changes or drop related properties from theVersionsListViewModel
and bind to theHomeViewModel.SelectedVersion
directly. A view model class per view/control will result in bad class design/code most of the time. Creating separate classes should be based on different considerations like responsibilities.And then you always want to avoid duplicate code (like properties and logic): instead of copying you move code to separate classes.