Data binding doesn't work with my dependency property - c#

I'm developing a WPF application with .NET Framework 4 and MVVM Light Toolkit
I created a custom user control which only contains a DataGrid:
<UserControl
x:Class="PadacEtl.Matcher.Views.LaraDataGrid"
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"
d:DesignHeight="300" d:DesignWidth="300"
DataContext="{Binding}">
<DataGrid ItemsSource="{Binding}" SelectionChanged="SelectionChanged">
<DataGrid.Columns>
<DataGridTextColumn Header="Value" Binding="{Binding Model.Value}" />
</DataGrid.Columns>
</DataGrid>
</UserControl>
This control defines a dependency property SelectedItems:
public partial class CustomDataGrid : UserControl
{
public IEnumerable<ItemViewModel> SelectedItems
{
get { return (IEnumerable<ItemViewModel>)GetValue(SelectedItemsProperty); }
set { SetValue(SelectedItemsProperty, value); }
}
public static readonly DependencyProperty SelectedItemsProperty =
DependencyProperty.Register("SelectedItems", typeof(IEnumerable<ItemViewModel>),
typeof(CustomDataGrid), new PropertyMetadata(new List<ItemViewModel>()));
public CustomDataGrid()
{
InitializeComponent();
}
private void SelectionChanged(object sender, SelectionChangedEventArgs e)
{
var dataGrid = sender as DataGrid;
SelectedItems = dataGrid.SelectedItems.Cast<ItemViewModel>();
}
}
Finally, this custom user control is used in a view, defined as follow:
<Window x:Class="Project.Views.MainView"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:uc="clr-namespace:Project.Views"
Title="Project"
Height="700" Width="1050"
DataContext="{Binding Source={StaticResource Locator}, Path=Main}">
<Window.Resources>
<ResourceDictionary Source="Styles.xaml" />
</Window.Resources>
<Grid>
<uc:CustomDataGrid DataContext="{Binding Items}"
SelectedItems="{Binding SelectedItems}" />
</Grid>
</Window>
With the corresponding ViewModel:
public class MainViewModel : ViewModelBase
{
public ObservableCollection<ItemViewModel> Items { get; private set; }
private IEnumerable<ItemViewModel> selectedItems = new List<ItemViewModel>();
public IEnumerable<ItemViewModel> SelectedItems
{
get { return selectedItems; }
set
{
if (value != selectedItems)
{
selectedItems = value;
RaisePropertyChanged(() => SelectedItems);
}
}
}
public MainViewModel()
{
//Something useful to feed Items
}
}
My problem is: When I select one or more rows from my CustomDataGrid, SelectedItems from MainViewModel is not updated. I think I didn't wired something well but I don't find what.
Any idea?

You have to have a two-way binding on your SelectedItems property. Either you do that explicitly in the binding expression like this:
<uc:CustomDataGrid ... SelectedItems="{Binding SelectedItems, Mode=TwoWay}"/>
or you set the FrameworkPropertyMetadataOptions.BindsTwoWayByDefault flags in the dependency property declaration:
public static readonly DependencyProperty SelectedItemsProperty =
DependencyProperty.Register(
"SelectedItems",
typeof(IEnumerable<ItemViewModel>),
typeof(CustomDataGrid),
new FrameworkPropertyMetadata(
null, FrameworkPropertyMetadataOptions.BindsTwoWayByDefault));
Note also that it is bad practice to set the default property value to a new collection instance, as that collection would be used as default value for all "instances" of the property. In other words, the default value of the SelectedItems property on two instances of CustomDataGrid would be the same collection object. If you add items to the one, the items would also be contained in the other. You have to set the default value in the constructor of the control.
Edit: After taking a second look at your UserControl and how you bind its properties, I realized that it can't work the way you designed it. Setting the DataContext as done in your binding declaration
<uc:CustomDataGrid DataContext="{Binding Items}"
SelectedItems="{Binding SelectedItems}"/>
would require to explicitly set the binding source object of the SelectedItems binding, perhaps like this
SelectedItems="{Binding SelectedItems, Source={StaticResource myViewModelInstance}}"
Instead of doing that, your control should have a bindable Items or ItemsSource in addition to the SelectedItems property. You could then simply write your bindings like this:
<uc:CustomDataGrid DataContext="{StaticResource myViewModelInstance}"
ItemsSource="{Binding Items}" SelectedItems="{Binding SelectedItems}"/>

Change the List to observablecollection, because observablecollection implements INotifyCollectionChanged and INotifyPropertyChanged where as list doesn't do so

Related

Why are listbox items not being unselected if they are not in view, after the listbox itself comes back into view?

This is a weird issue that I found in an MVVM project. I bound the IsSelected property of a ListBoxItem to an IsSelected property in the underlying model. If the collection holding the models bound to the list is too big, when you select a different user control and the focus is taken off of the ListBox; when you select an item in the list it will unselect every item EXCEPT the ones that are off-screen. The following gif shows this issue in a test project I made specifically for this issue;
MainView.xaml
<UserControl x:Class="ListBox_Selection_Issue.Views.MainView"
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:ListBox_Selection_Issue.Views"
xmlns:vms="clr-namespace:ListBox_Selection_Issue.ViewModels"
mc:Ignorable="d"
d:DesignHeight="450" d:DesignWidth="800">
<UserControl.DataContext>
<vms:MainViewModel/>
</UserControl.DataContext>
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="*"/>
<RowDefinition Height="*"/>
</Grid.RowDefinitions>
<ListBox Grid.Row="0" SelectionMode="Extended" ItemsSource="{Binding FirstCollection}">
<ListBox.ItemContainerStyle>
<Style TargetType="{x:Type ListBoxItem}">
<Setter Property="IsSelected" Value="{Binding IsSelected, Mode=TwoWay}" />
</Style>
</ListBox.ItemContainerStyle>
<ListBox.ItemTemplate>
<DataTemplate>
<TextBlock Text="{Binding Name}" />
</DataTemplate>
</ListBox.ItemTemplate>
</ListBox>
<Button Grid.Row="1"/>
</Grid>
</UserControl>
MainViewModel.cs
using System.Collections.ObjectModel;
namespace ListBox_Selection_Issue.ViewModels
{
class MainViewModel : ObservableObject
{
private ObservableCollection<CustomClass> _firstCollection;
public ObservableCollection<CustomClass> FirstCollection
{
get { return _firstCollection; }
set
{
_firstCollection = value;
OnPropertyChanged("FirstCollection");
}
}
public MainViewModel()
{
ObservableCollection<CustomClass> first = new ObservableCollection<CustomClass>();
for (int i = 1; i <= 300; i++)
{
first.Add(new CustomClass($"{i}"));
}
FirstCollection = first;
}
}
public class CustomClass
{
public string Name { get; set; }
public bool IsSelected { get; set; }
public CustomClass(string name)
{
Name = name;
IsSelected = false;
}
}
}
This is not how it works. If you understand UI virtualization, you should understand that virtualized containers (in your case ListBoxItem) are not part of the visual tree as they are removed as part of the virtualization process.
Because the WPF rendering engine has now far less containers to render, the performance is significantly improved. The effect becomes more relevant the more items the ItemsControl holds.
This is why you would never want to disable UI virtualization. This is why your posted solution can be qualified as a bad solution one should avoid.
ListBox is a Selector control. To allow it to work with any data, it must not be aware of the actual data models it renders. That's what the containers are for: anonymous wrappers that allow rendering and interaction logic without having the host to know the wrapped data object.
In your case, when ListBox.SelectionMode is set to SelectionMode.Extended or SelectionMode.Multiple, the ListBox will have to unselect all previously selected item in case the selection changes. Since it doesn't care about your data models, it only handles their associated wrappers: it will iterate over all ListBoxItem instances to change their state to e.g. unselected.
But the selection state will only be forwarded to the binding source for those Binding objects that are actually active (because the binding target is currently realized/visible).
Although all containers, virtualized and realized, will be unselected, the Binding of those virtualized containers won't update (because the corresponding container is not active and removed - removing includes clearing their ListBoxItem.Content property too).
As a matter of fact, unless container recycling is explicitly enabled by setting VirtualizingPanel.VirtualizationMode to VirtualizationMode.Recycling, virtualized container instances are removed by simply making them eligible for garbage collection. They are just dropped and gone without any further modification e.g., of the ListBoxItem.IsSelected property and new container instances are created for newly realized items.
Now when you scroll the virtualized items into the view, the ListBox will generate the containers and will set the ListBoxItem.Content property to the wrapped data item. Since in your case the data model, the CustomClass, still holds the previous and now outdated selection state (which is still "selected"), the realized containers will change their state back to selected (via the reactivated data binding).
That's why in your case virtualized items remain selected. And this is because you bind the container's state to your data model in a multiselect scenario with UI virtualization engaged.
ListBox selection states are meant to be handled via the Selector.SelectedItem and ListBox.SelectedItems properties or the Selector.SelectionChanged event. This is not important in single select mode but essential in multiselect mode.
Since ListBox.SelectedItems is a read-only property, you can't set a binding on it (like you would usually do with the SelectedItem property).
There are many ways you can get the SeletedItems value to your DataContext. The most straight forward would be to send it from the Selector.SelectionChanged event.
From a design perspective, you should generally never set the DataContext of a UserControl, or Control in general, explicitly. The DataContext should be inherited from the parent element that hosts your custom control.
A View-Model-per-View approach is always to avoid. It will make your control very specific to a particular data type. And vice versa, it will make your View Model too specific for a particular element of the View. Additionally, it will introduce other design problems that may even lead to break MVVM.
The DataContext must always be set outside the control (from the client context), so that the control doesn't know the concrete DataContext class.
The improved solution could look as follows:
MainWindow.xaml
<Window>
<Window.DataContext>
<local:MainViewModel />
</Window.DataContext>
<Grid>
<local:MainView ItemsSource="{Binding FirstCollection}"
SelectedItems="{Binding SelectedCustomClassModels}">
<local:MainView.ItemTemplate>
<DataTemplate>
<TextBlock Text="{Binding Name}" />
</DataTemplate>
</local:MainView.ItemTemplate>
</local:MainView>
</Grid>
</Window>
MainView.xaml.cs
public partial class MainView : UserControl
{
public IList SelectedItems
{
get => (IList)GetValue(SelectedItemsProperty);
set => SetValue(SelectedItemsProperty, value);
}
public static readonly DependencyProperty SelectedItemsProperty = DependencyProperty.Register(
"SelectedItems",
typeof(IList),
typeof(MainView),
new FrameworkPropertyMetadata(default(IList), FrameworkPropertyMetadataOptions.BindsTwoWayByDefault));
public IList ItemsSource
{
get => (IList)GetValue(ItemsSourceProperty);
set => SetValue(ItemsSourceProperty, value);
}
public static readonly DependencyProperty ItemsSourceProperty = DependencyProperty.Register(
"ItemsSource",
typeof(IList),
typeof(MainView), new PropertyMetadata(default));
public DataTemplate ItemTemplate
{
get => (DataTemplate)GetValue(ItemTemplateProperty);
set => SetValue(ItemTemplateProperty, value);
}
public static readonly DependencyProperty ItemTemplateProperty = DependencyProperty.Register(
"ItemTemplate",
typeof(DataTemplate),
typeof(MainView), new PropertyMetadata(default));
public MainView()
{
InitializeComponent();
}
private void ListBox_SelectionChanged(object sender, SelectionChangedEventArgs e)
{
// You can track particular items...
IList newSelectedItems = e.AddedItems;
IList newUnselectedItems = e.RemovedItems;
// ... or the final result
var listBox = sender as ListBox;
this.SelectedItems = listBox.SelectedItems;
}
}
MainView.xaml
<UserControl>
<Grid>
<Grid.RowDefinitions>
<RowDefinition />
<RowDefinition />
</Grid.RowDefinitions>
<ListBox Grid.Row="0"
SelectionMode="Extended"
SelectionChanged="ListBox_SelectionChanged"
ItemsSource="{Binding RelativeSource={RelativeSource AncestorType=UserControl}, Path=ItemsSource}"
ItemTemplate="{Binding RelativeSource={RelativeSource AncestorType=UserControl}, Path=ItemTemplate}" />
<Button Grid.Row="1" />
</Grid>
</UserControl>
MainViewModel.cs
public class MainViewModel : INotifyPropertyChanged
{
public ObservableCollection<CustomClass> FirstCollection { get; private set; }
private IList selectedCustomClassModels;
public IList SelectedCustomClassModels
{
get => this.selectedCustomClassModels;
set
{
this.selectedCustomClassModels = value;
OnPropertyChanged();
OnSelectedCustomClassModelsChanged();
}
}
public MainViewModel()
{
this.FirstCollection = new ObservableCollection<CustomClass>();
for (int i = 1; i <= 300; i++)
{
this.FirstCollection.Add(new CustomClass($"{i}"));
}
this.SelectedCustomClassModels = new List<object>();
}
private void OnSelectedCustomClassModelsChanged()
{
// TODO::Handle selected items
IEnumerable<CustomClass> selectedItems = this.SelectedCustomClassModels
.Cast<CustomClass>();
}
}
Finding the fix to this issue took me longer than I would have expected, so I figured I would share my knowledge.
Add VirtualizingStackPanel.IsVirtualizing="False" to the ListBox like in the following file:
<UserControl x:Class="ListBox_Selection_Issue.Views.MainView"
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:ListBox_Selection_Issue.Views"
xmlns:vms="clr-namespace:ListBox_Selection_Issue.ViewModels"
mc:Ignorable="d"
d:DesignHeight="450" d:DesignWidth="800">
<UserControl.DataContext>
<vms:MainViewModel/>
</UserControl.DataContext>
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="*"/>
<RowDefinition Height="*"/>
</Grid.RowDefinitions>
<ListBox Grid.Row="0" SelectionMode="Extended" VirtualizingStackPanel.IsVirtualizing="False" ItemsSource="{Binding FirstCollection}">
<ListBox.ItemContainerStyle>
<Style TargetType="{x:Type ListBoxItem}">
<Setter Property="IsSelected" Value="{Binding IsSelected, Mode=TwoWay}" />
</Style>
</ListBox.ItemContainerStyle>
<ListBox.ItemTemplate>
<DataTemplate>
<TextBlock Text="{Binding Name}" />
</DataTemplate>
</ListBox.ItemTemplate>
</ListBox>
<Button Grid.Row="1"/>
</Grid>
</UserControl>
I found out that the issue had to do with "Virtualization" from this answer (Select Show more comments). Then I started looking into Listbox Virtualization. I lost the tab where I got the answer from. So, I can't credit them. Hopefully, this will help someone in the future.

MVVM - get selected items from ListBox via Data Binding

I am writing app using MVVM pattern in C#.
My goal is to get selected items from ListBox in my own User Control.
I have created bindable object, with method to change this object (called when something new is selected):
public partial class MyUserControl : UserControl
{
...
public IEnumerable SelectedItems
{
get { return (IEnumerable)GetValue(SelectedItemsProperty); }
set { SetValue(SelectedItemsProperty, value); }
}
public static readonly DependencyProperty SelectedItemsProperty =
DependencyProperty.Register("SelectedItems", typeof(IEnumerable),
typeof(MyUserControl),
new FrameworkPropertyMetadata(default(IEnumerable),
FrameworkPropertyMetadataOptions.BindsTwoWayByDefault));
public MyUserControl ()
{
InitializeComponent();
}
private void SelectionChanged(object sender, SelectionChangedEventArgs e)
{
SelectedItems = ListBox.SelectedItems;
}
}
There is also Items part, and in xaml part ListBox is named ListBox:
<ListBox Name="ListBox" SelectionChanged="SelectionChanged" ... />
This how it looks in page with ViewModel, in which MyUserControl is created:
<uc:MyUserControl ... SelectedItems="{Binding Path=MyObjectItems}" />
And here comes the problem. When setting SelectedItems in ViewModel:
private ObservableCollection<MyObject> _myObjectItems;
public ObservableCollection<MyObject> MyObjectItems
{
get { return _myObjectItems; }
set { _myObjectItems = value; }
}
No matter what I do, value will always be null. This also means, that SelectedItems in MyUserControl is null, too.
I can, for example use OneWayToSource binding mode:
<uc:MyUserControl ... SelectedItems="{Binding Path=MyObjectItems, Mode=OneWayToSource}" />
value is still null, same as MyObjectItems, but at least SelectedItems in MyUserControl contains selected items. Not good enough :/
After hours of trying different approaches I've found NuGet package Extended WPF Toolkit. List SelectedItemsOverride from class CheckListBox allows to bind list of selected items:
<UserControl x:Class="View.UserControls.MyUserControl"
...
xmlns:xctk="http://schemas.xceed.com/wpf/xaml/toolkit"
x:Name="Root">
<Grid>
<xctk:CheckListBox Name="ListBox"
ItemSelectionChanged="SelectionChanged"
SelectedItemsOverride="{Binding SelectedItems, ElementName=Root}"
... />
</Grid>
</UserControl>
And because of that, binding:
<uc:MyUserControl ... SelectedItems="{Binding Path=MyObjectItems}" />
works! I have access to selected items in View Model - everything in simple way.

WPF DataGridComboBoxColumn not working

I have a DataGridComboBoxColumn in a DataGrid in a WPF project set like this:
<DataGridComboBoxColumn Header="Master" SelectedItemBinding="{Binding MasterId}" SelectedValueBinding="{Binding Id}" DisplayMemberPath="Id" ItemsSource="{Binding Masters}" />
but when I run the project the column display only blank values and the combobox in edit mode does the same thing.
The DataGrid is set like this:
<DataGrid Name="ReadersGrid" Grid.Row="0" Grid.Column="0" Margin="3" ItemsSource="{Binding Readers}" CanUserAddRows="True" CanUserDeleteRows="True" AutoGenerateColumns="False">
And the UserControl like this:
<UserControl x:Class="SmartAccess.Tabs.ReadersTab"
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:SmartAccess.Tabs"
mc:Ignorable="d"
d:DesignHeight="300" d:DesignWidth="300" DataContext="{StaticResource ReadersListViewModel}">
and the other columns, only text, work fine.
The ViewModel has these properties
public ObservableCollection<ReaderViewModel> Readers { get; set; }
public IEnumerable<ReaderViewModel> Masters => Readers.Concat(new List<ReaderViewModel> { new ReaderViewModel { Id = -1 } }).OrderBy(t => t.Id);
And the collection viewmodel has these properties
public long Id { get; set; }
public long MasterId { get; set; }
I'm displaying Id only for test, a description property will be added in future.
Why the ComboBoxColumn is not working?
Your issue is caused by DataGridColumns: indeed they do not belong to the visual tree, so you cannot bind their properties to your DataContext.
You can find here a solution based on a kind of freezable "DataContext proxy", since Freezable objects can inherit the DataContext even when they are not in the visual tree.
Now if you put this proxy in the DataGrid's resources, it can be bound the the DataGrid's DataContext and can be retrieve by using the StaticResource keyword.
So you XAML will become:
<DataGridComboBoxColumn Header="Master" SelectedItemBinding="{Binding MasterId}"
SelectedValueBinding="{Binding Id}" DisplayMemberPath="Id"
ItemsSource="{Binding Data.Masters, Source={StaticResource proxy}}" />
Where proxy is the name of your resource.
I hope it can help you.
EDIT
I update my answer with the code copied from this link (because of #icebat's comment). This is the BindingProxy class:
public class BindingProxy : Freezable
{
#region Overrides of Freezable
protected override Freezable CreateInstanceCore()
{
return new BindingProxy();
}
#endregion
public object Data
{
get { return (object)GetValue(DataProperty); }
set { SetValue(DataProperty, value); }
}
// Using a DependencyProperty as the backing store for Data. This enables animation, styling, binding, etc...
public static readonly DependencyProperty DataProperty =
DependencyProperty.Register("Data", typeof(object), typeof(BindingProxy), new UIPropertyMetadata(null));
}
Then in the XAML you need to add:
<DataGrid.Resources>
<local:BindingProxy x:Key="proxy" Data="{Binding}" />
</DataGrid.Resources>

binding to nested object from the parent user control wpf

I am writing a new user control. It needs to be able to display an ObservableCollection of items. Those items will have a property that is also an observable collection, so it is similar to a 2-d jagged array. The control is similar to a text editor so the outer collection would be the lines, the inner collection would be the words. I want the consumer of the control to be able to specify not only the binding for the lines, but also the binding for the words. The approach I have so far is as follows:
The user control inherits from ItemsControl. Inside this control it has a nested ItemsControl. I would like to be able to specify the binding path of this nested ItemsControl from the parent user control. The XAML for the UserControl is
<ItemsControl x:Class="IntelliDoc.Client.Controls.TextDocumentEditor"
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:IntelliDoc.Client"
xmlns:con="clr-namespace:IntelliDoc.Client.Controls"
xmlns:data="clr-namespace:IntelliDoc.Data;assembly=IntelliDoc.Data"
xmlns:util="clr-namespace:IntelliDoc.Client.Utility"
xmlns:vm="clr-namespace:IntelliDoc.Client.ViewModel"
xmlns:sys="clr-namespace:System;assembly=mscorlib"
xmlns:i="http://schemas.microsoft.com/expression/2010/interactivity"
mc:Ignorable="d"
x:Name="root"
d:DesignHeight="300" d:DesignWidth="300"
>
<ItemsControl.Template>
<ControlTemplate>
<StackPanel Orientation="Vertical">
<ItemsPresenter Name="PART_Presenter" />
</StackPanel>
</ControlTemplate>
</ItemsControl.Template>
<ItemsControl.ItemTemplate>
<DataTemplate >
<StackPanel Orientation="Horizontal">
<ItemsControl Name="PART_InnerItemsControl" ItemsSource="{Binding NestedBinding, ElementName=root}" >
<ItemsControl.Template>
<ControlTemplate>
<StackPanel Name="InnerStackPanel" Orientation="Horizontal" >
<TextBox Text="" BorderThickness="0" TextChanged="TextBox_TextChanged" />
<ItemsPresenter />
</StackPanel>
</ControlTemplate>
</ItemsControl.Template>
<ItemsControl.ItemTemplate>
<DataTemplate>
<StackPanel Orientation="Horizontal" >
<ContentControl Content="{Binding Path=Data, Mode=TwoWay}" />
<TextBox BorderThickness="0" TextChanged="TextBox_TextChanged" />
</StackPanel>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
</StackPanel>
</DataTemplate>
</ItemsControl.ItemTemplate>
The code behind has this property declared
public partial class TextDocumentEditor : ItemsControl
{
public static readonly DependencyProperty NestedItemsProperty = DependencyProperty.Register("NestedItems", typeof(BindingBase), typeof(TextDocumentEditor),
new PropertyMetadata((BindingBase)null));
public BindingBase NestedItems
{
get { return (BindingBase)GetValue(NestedItemsProperty); }
set
{
SetValue(NestedItemsProperty, value);
}
}
...
}
The expected bound object will be as follows:
public class ExampleClass
{
ObservableCollection<InnerClass> InnerItems {get; private set;}
}
public class InnerClass : BaseModel //declares OnPropertyChanged
{
private string _name;
public string Name //this is provided as an example property and is not required
{
get
{
return _name;
}
set
{
_name = value;
OnPropertyChanged(nameof(Name));
}
}
....
}
public class ViewModel
{
public ObservableCollection<ExampleClass> Items {get; private set;}
}
The XAML declaration would be as follows:
<Window x:Class="IntelliDoc.Client.TestWindow"
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:sys="clr-namespace:System;assembly=mscorlib"
mc:Ignorable="d"
Title="TestWindow" Height="300" Width="300">
<DockPanel>
<TextDocumentEditor ItemsSource="{Binding Path=Items}" NestedItems={Binding Path=InnerItems} >
<DataTemplate>
<!-- I would like this to be the user defined datatemplate for the nested items. Currently I am just declaring the templates in the resources of the user control by DataType which also works -->
</DataTemplate>
</TextDocumentEditor>
</DockPanel>
In the end, I want the user control I created to provide the ItemsControl template at the outer items level, but I want the user to be able to provide the datatemplate at the inner items control level. I want the consumer of the control to be able to provide the bindings for both the Outer items and the nested items.
I was able to come up with a solution that works for me. There may be a better approach, but here is what I did.
First, on the outer ItemsControl, I subscribed to the StatusChanged of the ItemContainerGenerator. Inside that function, I apply the template of the ContentPresenter and then search for the Inner ItemsControl. Once found, I use the property NestedItems to bind to the ItemsSource property. One of the problems I was having originally was I was binding incorrectly. I fixed that and I changed the NestedItems to be a string. Also, I added a new property called NestedDataTemplate that is of type DataTemplate so that a user can specify the DataTemplate of the inner items control. It was suggested that I not use a UserControl since I don't inherit from a UserControl, so I will change it to a CustomControl. The code changes are below
public static readonly DependencyProperty NestedItemsProperty = DependencyProperty.Register("NestedItems", typeof(string), typeof(TextDocumentEditor),
new PropertyMetadata((string)null));
public static readonly DependencyProperty NestedDataTemplateProperty = DependencyProperty.Register("NestedDataTemplate", typeof(DataTemplate), typeof(TextDocumentEditor),
new PropertyMetadata((DataTemplate)null));
public DataTemplate NestedDataTemplate
{
get { return (DataTemplate)GetValue(NestedDataTemplateProperty); }
set
{
SetValue(NestedDataTemplateProperty, value);
}
}
public string NestedItems
{
get { return (string)GetValue(NestedItemsProperty); }
set
{
SetValue(NestedItemsProperty, value);
}
}
private void ItemContainerGenerator_StatusChanged(object sender, EventArgs e)
{
if (((ItemContainerGenerator)sender).Status != GeneratorStatus.ContainersGenerated)
return;
ContentPresenter value;
ItemsControl itemsControl;
for (int x=0;x<ItemContainerGenerator.Items.Count; x++)
{
value = ItemContainerGenerator.ContainerFromIndex(x) as ContentPresenter;
if (value == null)
continue;
value.ApplyTemplate();
itemsControl = value.GetChildren<ItemsControl>().FirstOrDefault();
if (itemsControl != null)
{
if (NestedDataTemplate != null)
itemsControl.ItemTemplate = NestedDataTemplate;
Binding binding = new Binding(NestedItems);
BindingOperations.SetBinding(itemsControl, ItemsSourceProperty, binding);
}
}
}

WPF Binding Combox with different List and different SelectedValue

In my UserControl ucStep2 I have DataContext of Step2InfoData object that has several properties along with :
private string rockDensUnit;
public string RockDensity_Unit
{
get { return rockDensUnit; }
set
{
if (rockDensUnit != value)
{
rockDensUnit = value;
Changed("RockDensity_Unit");
}
}
}
In my app I got to bind several combo's with different normally measurement types Like {kg/m3, gm/m3}, {meter, cm} and so on such groups of measures. I mean, multiple combo's to have list of same items. So I preferred to create Class's of such lists that I can use in multiple combos. I created ComboItems.cs which contains all items lists that I will need to populate the drop down.
ComboItems.cs
//**OBJECTS I USE FOR LIST OF IEMS**
// Class for kg, gm
public class KgGmItems
{
public ObservableCollection<string> KgGmList { get; set; }
public KgGmItems()
{
KgGmList = new ObservableCollection<string>();
KgGmList.Add("kg/m3");
KgGmList.Add("gram/cm3");
}
public string ValueSelected { get; set; } // Don't know if this is useful in my case
}
// Class for meter, cm
public class MtCmItems : INotifyPropertyChanged
{
public MtCmItems()
{
Dict = new Dictionary<string, string>
{
{"meter", "meter"},
{"centimeter", "centimeter"}
};
}
//...
}
XML i.e. ucStep2 View
<!-- As the objects KgGmItems doesn't contain in ucStep2.xaml.cs or Step2InfoData (that is bound to this UC) so add reference of those classes -->
<UserControl.Resources>
<ObjectDataProvider x:Key="KgGmObj" ObjectType="{x:Type top:KgGmItems}" />
<ObjectDataProvider x:Key="MtCmObj" ObjectType="{x:Type top:MtCmItems}" />
</UserControl.Resources>
<ComboBox DataContext="{StaticResource KgGmObj}" ItemsSource="{Binding KgGmList}" SelectedValue="{Binding Path=RockDensity_Unit, Mode=TwoWay}" SelectedIndex="0"
Background="#FFB7B39D" Grid.Row="5" Height="23" HorizontalAlignment="Left" Margin="401,61,0,0" Name="comboBox6" VerticalAlignment="Top" Width="84" Visibility="Hidden">
</ComboBox>
I want to display ObservableCllection KgGmList items from KgGmItems class and bind the selected value to RockDensity_Unit of class Step2InfoData that is bound to this UserControl.
In the above combo, I am able to display all items in the drop down, also 1st item is selected by default. But the value is not bind to RockDensity_Unit; it's value remains null.
I want this to happen 2-way i.e. when RockDensity_Unit proeprtiy's value is set programmatically, the value should be selected in the drop down. Of course the value should exists in the list.
By default the 1st item should be selected.
UPDATE
Added DependencyProperty in ucStep2.xaml.cs
public static readonly DependencyProperty RockDensityUnitProperty =
DependencyProperty.Register("RockDensity_Unit", typeof(string), typeof(UserControl),
new FrameworkPropertyMetadata("kg/m3", FrameworkPropertyMetadataOptions.BindsTwoWayByDefault));
public string RockDensity_Unit
{
get { return this.GetValue(RockDensityUnitProperty) as string; }
set { SetValue(RockDensityUnitProperty, value); }
}
XML
<ComboBox DataContext="{StaticResource KgGmObj}" ItemsSource="{Binding KgGmList}" SelectedItem="{Binding Path=RockDensity_Unit, RelativeSource={RelativeSource Mode=FindAncestor, AncestorType={x:Type ucStep2}}, Mode=TwoWay}"
Background="#FFB7B39D" Grid.Row="5" Height="23" HorizontalAlignment="Left" Margin="401,61,0,0" Name="comboBox6" VerticalAlignment="Top" Width="84" Visibility="Hidden">
</ComboBox>
ERROR
Error 1 The type reference cannot find a public type named 'ucStep2'. Line 74 Position 194. This refers to the combobox ", "
after FindAncestor
DOUBT
The RockDensity_Unit CLR property in Step2InfoData is untouched.
Why is the code not able to find ucStep2 ? FYI, I think this may be relevant :
<UserControl x:Class="WellBore.ucStep2"
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:WellBore.Models"
xmlns:top="clr-namespace:WellBore"
mc:Ignorable="d"
d:DesignHeight="870" d:DesignWidth="700" MaxHeight="970" MinHeight="700" MaxWidth="600">
Ok, so let's get this binding working... first, I am using an item from your KgGmItems class to bind to the ComboBox. In this class you have a collection of string values to display in the drop down and a string property to bind to the ComboBox.SelectedItem... perfect! Now I'm assuming that you have an instance of this class in the Resources section called KgGmObj... let's keep it simple to start with:
<ComboBox DataContext="{StaticResource KgGmObj}" ItemsSource="{Binding KgGmList}"
SelectedItem="{Binding ValueSelected, Mode=TwoWay}" />
This is all you need to setup the binding between the ComboBox and your class. One thing to note though, is that when you try to set the selected item from your code, it will only work if you set it to one of the actual items in the collection... I think that this doesn't really count when using strings, but it's important to know this anyway. If you were setting a custom class as the type of objects in the ComboBox instead, then you could set the selected item like this:
ValueSelected = KgGmList.Where(item => item.Name == "NameOfObjectToMatch").Single();
Or better like this if you had a uniquely identifiable property:
ValueSelected = KgGmList.Where(item => item.Id == Id).Single()
With your string values, you should be able to set the selected item from code like this:
ValueSelected = "Some value";
UPDATE >>> Ok, so let's have another go... I think that I may have enough information to go on now. I think that you want something like this:
<ComboBox DataContext="{StaticResource KgGmObj}" ItemsSource="{Binding KgGmList}"
SelectedItem="{Binding RockDensity_Unit, Mode=TwoWay}" />
The problem with this is that you have set the DataContext of the ComboBox to your KgGmObj object. This means that the Framework is going to try to find a property named RockDensity_Unit in that object. I also see another potential problem in your definition of this property.
In order to bind from a UserControl xaml to its code behind, you need to use a DependencyProperty. You can find out how to implement these from the Dependency Properties Overview page at MSDN. So first, I would recommend that you implement your RockDensity_Unit property as a DependencyProperty.
Next, we have to find a way to that property from the ComboBox in the xaml... we can do that using a RelativeSource binding like this:
<ComboBox DataContext="{StaticResource KgGmObj}" ItemsSource="{Binding KgGmList}"
SelectedItem="{Binding RockDensity_Unit, RelativeSource={RelativeSource Mode=
FindAncestor, AncestorType={x:Type ucStep2}}, Mode=TwoWay}" />
Now, if you have a DependencyProperty to bind to the SelectedItem property and your UserControl class is named ucStep2, this should all work... let me know how it goes.
UPDATE 2 >>>
Your error is because you have to add an XML namespace at the top of your XAML file... something like this:
xmlns:YourNamespace="clr-namespace:ApplicationName.FolderNameContainingClass"
Then you use it to reference your class like this:
...AncestorType={x:Type YourNamespace:ucStep2} ...
Also, in your DependencyProperty declaration, you're supposed to supply the name the type of your control, not UserControl, so change
Register("RockDensity_Unit", typeof(string), typeof(UserControl),
to
Register("RockDensity_Unit", typeof(string), typeof(NameOfYourUserControl),
Clearly... replace 'NameOfYourUserControl' with the actual name of your class that extends the UserControl.
Use a Dictionary.
XAML
<ComboBox ItemsSource="{Binding Dict}"
DisplayMemberPath="Value"
SelectedValuePath="Key"
SelectedValue="{Binding Prop}"/>
Code Behind
public Dictionary< ValueType, string > Dict { get; private set; }
private ValueType _prop;
public ValueType Prop
{
get{ return _prop }
set
{
_prop = value;
NotifyPropertyChanged( "Prop" ); // Implement INotifyPropertyChanged
}
}
public ViewModel()
{
Dict = new Dictionary< ValueType, string >()
{
{ value1, string1 },
{ value2, string2 },
{ value3, string3 }
};
}

Categories