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.CoerceValuefrom 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.AffectsMeasureflag as it will force a complete layout pass every time the property changes, which is unnecessary in your case. TheListBox.SelectedItemhas 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.BindsTwoWayByDefaultflag.a) Bind internals of a
Controlto the control's properties and not to theDataContext. Otherwise your control becomes inconvenient to handle (and to write). For example, if you change theDataContextor 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
DataContextbindings and introduce a dependency property for each. For example, remove theListBox.ItemsSourcebinding to theVersionsListViewModel.Versionsproperty and introduce a e.g.,VersionsItemsSourcedependency property instead.For example in
HomeView: define allDataContextrelatedBindingrelative to the actualDataContextof theUserControl(orFrameworkElementin general) instead of starting a traversal (usingBinding.RelativeSource) to find the parent'sDataContextthat 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.
