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
CodePudding user response:
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
public partial class VersionsList : UserControl
{
public VersionPcVue SelectedVersionItem
{
get => (VersionPcVue)GetValue(SelectedVersionItemProperty);
set => SetValue(SelectedVersionItemProperty, value);
}
public static readonly DependencyProperty SelectedVersionItemProperty = DependencyProperty.Register(
"SelectedVersionItem",
typeof(VersionPcVue),
typeof(VersionsList),
new FrameworkPropertyMetadata(default(VersionPcVue), FrameworkPropertyMetadataOptions.BindsTwoWayByDefault, OnSelectedVersionChanged));
public IList VersionsItemsSource
{
get => (IList)GetValue(VersionsItemsSourceProperty);
set => SetValue(VersionsItemsSourceProperty, value);
}
public static readonly DependencyProperty VersionsItemsSourceProperty = DependencyProperty.Register(
"VersionsItemsSource",
typeof(IList),
typeof(VersionsList),
new PropertyMetadata(default));
public VersionsList()
{
InitializeComponent();
}
private static void OnSelectedVersionChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
}
}
3a
VersionList.xaml
When authoring a Control
, always bind to properties of the control instead to properties of the DataContext
:
<UserControl>
<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 RelativeSource={RelativeSource AncestorType=UserControl}, Path=VersionsItemsSource}"
SelectedItem="{Binding RelativeSource={RelativeSource AncestorType=UserControl}, Path=SelectedVersionItem}">
<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>
4
HomeView.xaml
Note that the DataContext
of the VersionsList
control is referencing a VersionsListViewModel
instance. You must adjust all bindings accordingly:
<UserControl>
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*" />
<ColumnDefinition Width="*" />
<ColumnDefinition Width="2*" />
</Grid.ColumnDefinitions>
<Controls:VersionsList x:Name="test"
Grid.Column="0"
DataContext="{Binding VersionsListViewModel}"
VersionsItemsSource="{Binding Versions}"
SelectedVersionItem="{Binding SelectedVersion}" />
</Grid>
</UserControl>
Remarks
"Why can't I get back the Selected Item back to the SelectedVersion property of my HomeViewModel."
Given your current class design and control configuration, your VersionsList
control binds to the VersionsListViewModel.SelectedVersion
property. It's not clear what you really want at this point.
Either delegate the value manually by letting the HomeViewModel
listen to VersionsListViewModel.SelectedVersion
property changes or drop related properties from the VersionsListViewModel
and bind to the HomeViewModel.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.
CodePudding user response:
In VersionList.xaml
:
<ListBox SelectedItem="{Binding SelectedVersion}" ...
This only bind ListBox.SelectedItem
to {DataContext}.SelectedVersion
. Then when a item is selected, the dependency property VersionList.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
, remove SelectedVersionProperty
and SelectedVersion
members.
Keep VersionList.xaml
with :
<UserControl x:Class="PcVueLauncher.Controls.VersionsList" />
...
<ListBox
...
ItemsSource="{Binding Versions}"
SelectedItem="{Binding SelectedVersion}">
...
</UserControl>
So ListBox.SelectedItem
is bind to ListBox.DataContext.SelectedVersion
. If ListBox.DataContext
is VersionsListViewModel
, then ListBox.SelectedItem
is bind to VersionsListViewModel.SelectedVersion
.
In the parent controls HomeView
, it only need to pass a VersionsListViewModel
to the VersionList.DataContext
:
<UserControl x:Class="PcVueLauncher.Views.HomeView"
...
<Controls:VersionsList DataContext="{Binding VersionsListViewModel}" />
...
</UserControl>
So HomeView.VersionsList.ListBox.SelectedItem
is bind HomeView.DataContext.VersionsListViewModel.SelectedVersion
. If HomeView.DataContext
is HomeViewModel
, then HomeView.VersionsList.ListBox.SelectedItem
is bind HomeViewModel.VersionsListViewModel.SelectedVersion
.
Finally, you can remove the member HomeViewModel.SelectedVersion
and use HomeViewModel.VersionsListViewModel.SelectedVersion
.
If you want keep the member HomeViewModel.SelectedVersion
, then you need to redirect HomeViewModel.SelectedVersion
to HomeViewModel.VersionsListViewModel.SelectedVersion
in HomeViewModel.cs
:
public class HomeViewModel : ViewModelBase
{
private VersionsListViewModel versionsListViewModel;
public VersionsListViewModel VersionsListViewModel
{
get
{
return versionsListViewModel;
}
set
{
if(versionsListViewModel != null)
versionsListViewModel.PropertyChanged -= VersionsListViewModel_PropertyChanged;
versionsListViewModel = value;
if(versionsListViewModel != null)
versionsListViewModel.PropertyChanged = VersionsListViewModel_PropertyChanged;
OnPropertyChanged(nameof(VersionsListViewModel));
}
}
public VersionPcVue SelectedVersion
{
get
{
return versionsListViewModel.SelectedVersion;
}
set
{
versionsListViewModel.SelectedVersion = value;
OnPropertyChanged(nameof(SelectedVersion));
}
}
void VersionsListViewModel_PropertyChanged(object sender, PropertyChangedEventArgs e)
{
//Propagate the property changed SelectedVersion
if(string.IsNullOrEmpty(e.PropertyName) || e.PropertyName == nameof(VersionsListViewModel.SelectedVersion))
OnPropertyChanged(nameof(SelectedVersion));
}
}
The trick is when HomeViewModel.VersionsListViewModel.SelectedVersion
is changed, also notify that HomeViewModel.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 bind ListBox.SelectedItem
to VersionsList.SelectedVersion
.
First, add the dependency property SelectedVersion
in VersionList.cs
:
public partial class VersionsList : UserControl
{
public VersionsList()
{
InitializeComponent();
}
public VersionPcVue SelectedVersion
{
get { return (VersionPcVue)GetValue(SelectedVersionProperty); }
set { SetValue(SelectedVersionProperty, value); }
}
public static readonly DependencyProperty SelectedVersionProperty =
DependencyProperty.Register(
"SelectedVersion",
typeof(VersionPcVue),
typeof(VersionsList),
new FrameworkPropertyMetadata(
defaultValue: null,
flags: FrameworkPropertyMetadataOptions.AffectsMeasure
)
);
public List<VersionPcVue> Versions
{
get { return (List<VersionPcVue>)GetValue(VersionsProperty); }
set { SetValue(VersionsProperty, value); }
}
public static readonly DependencyProperty VersionsProperty =
DependencyProperty.Register(
"Versions",
typeof(List<VersionPcVue>),
typeof(VersionsList),
new FrameworkPropertyMetadata(
defaultValue: null,
flags: FrameworkPropertyMetadataOptions.AffectsMeasure
)
);
}
In VersionList.xaml
:
<UserControl x:Class="PcVueLauncher.Controls.VersionsList" />
...
<ListBox
...
ItemsSource="{Binding Versions}, RelativeSource={RelativeSource TemplatedParent}, Mode=TwoWay}"
SelectedItem="{Binding SelectedVersion, RelativeSource={RelativeSource TemplatedParent}, Mode=TwoWay}">
...
</UserControl>
{RelativeSource TemplatedParent}
indicate the binding refer to the element to which the template, here VersionsList
.
To use the control :
<UserControl x:Class="PcVueLauncher.Views.HomeView"
...
<Controls:VersionsList
Versions="{Binding VersionsListViewModel.Versions}"
SelectedVersion="{Binding SelectedVersion}"/>
...
</UserControl>
Versions is also changed to harmonize the binding strategy.
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.