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>
Related
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.
I have an ObservableCollection of DataPoint objects. That class has a Data property. I have a DataGrid. I have a custom UserControl called NumberBox, which is a wrapper for a TextBox but for numbers, with a Value property that is bind-able (or at least is intended to be).
I want the DataGrid to display my Data in a column NumberBox, so the value can be displayed and changed. I've used a DataGridTemplateColumn, bound the Value property to Data, set the ItemsSource to my ObservableCollection.
When the underlying Data is added or modified, the NumberBox updates just fine. However, when I input a value in the box, the Data doesn't update.
I've found answers suggesting to implement INotifyPropertyChanged. Firstly, not sure on what I should implement it. Secondly, I tried to implement it thusly on both my DataPoint and my NumberBox. I've found answers suggesting to add Mode=TwoWay, UpdateSourceTrigger=PropertyChange to my Value binding. I've tried both of these, separately, and together. Obviously, the problem remains.
Below is the bare minimum project I'm currently using to try to make this thing work. What am I missing?
MainWindow.xaml
<Window xmlns:BindingTest="clr-namespace:BindingTest" x:Class="BindingTest.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Title="MainWindow" Height="350" Width="525">
<Grid>
<DataGrid Name="Container" AutoGenerateColumns="False" CanUserSortColumns="False" CanUserResizeColumns="False" CanUserResizeRows="False" CanUserReorderColumns="False" CanUserAddRows="False" CanUserDeleteRows="True">
<DataGrid.Columns>
<DataGridTemplateColumn Header="Sample Text" Width="100" >
<DataGridTemplateColumn.CellTemplate>
<DataTemplate>
<BindingTest:NumberBox Value="{Binding Data}"/>
</DataTemplate>
</DataGridTemplateColumn.CellTemplate>
</DataGridTemplateColumn>
</DataGrid.Columns>
</DataGrid>
<Button Name="BTN" Click="Button_Click" Height="30" VerticalAlignment="Bottom" Content="Check"/>
</Grid>
</Window>
MainWindow.cs
public partial class MainWindow : Window
{
private ObservableCollection<DataPoint> Liste { get; set; }
public MainWindow()
{
InitializeComponent();
Liste = new ObservableCollection<DataPoint>();
Container.ItemsSource = Liste;
DataPoint dp1 = new DataPoint(); dp1.Data = 1;
DataPoint dp2 = new DataPoint(); dp2.Data = 2;
Liste.Add(dp1);
Liste.Add(dp2);
}
private void Button_Click(object sender, RoutedEventArgs e)
{
BTN.Content = Liste[0].Data;
}
}
DataPoint.cs
public class DataPoint
{
public double Data { get; set; }
}
NumberBox.xaml
<UserControl x:Class="BindingTest.NumberBox"
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="28" d:DesignWidth="200">
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*"/>
<ColumnDefinition Width="Auto"/>
</Grid.ColumnDefinitions>
<TextBox Name="Container" HorizontalAlignment="Stretch" VerticalAlignment="Stretch" VerticalContentAlignment="Center"/>
</Grid>
</UserControl>
NumberBox.cs
public partial class NumberBox : UserControl
{
public event EventHandler ValueChanged;
public NumberBox()
{
InitializeComponent();
}
private double _value;
public double Value
{
get { return _value; }
set
{
_value = value;
Container.Text = value.ToString(CultureInfo.InvariantCulture);
if (ValueChanged != null) ValueChanged(this, new EventArgs());
}
}
public static readonly DependencyProperty ValueProperty = DependencyProperty.Register(
"Value",
typeof(double),
typeof(NumberBox),
new PropertyMetadata(OnValuePropertyChanged)
);
public static void OnValuePropertyChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
double? val = e.NewValue as double?;
(d as NumberBox).Value = val.Value;
}
}
What am I missing?
Quite a few things actually.
The CLR property of a dependency property should only get and set the value of the dependency property:
public partial class NumberBox : UserControl
{
public NumberBox()
{
InitializeComponent();
}
public double Value
{
get => (double)GetValue(ValueProperty);
set => SetValue(ValueProperty, value);
}
public static readonly DependencyProperty ValueProperty = DependencyProperty.Register(
"Value",
typeof(double),
typeof(NumberBox),
new PropertyMetadata(0.0)
);
private void Container_PreviewTextInput(object sender, TextCompositionEventArgs e)
{
Value = double.Parse(Container.Text, CultureInfo.InvariantCulture);
}
}
The TextBox in the control should bind to the Value property:
<TextBox Name="Container" HorizontalAlignment="Stretch" VerticalAlignment="Stretch" VerticalContentAlignment="Center"
Text="{Binding Value, RelativeSource={RelativeSource AncestorType=UserControl}}"
PreviewTextInput="Container_PreviewTextInput"/>
The mode of the binding between the Value and the Data property should be set to TwoWay and also, and this is because the input control is in the CellTemplate of the DataGrid, the UpdateSourceTrigger should be set to PropertyChanged:
<local:NumberBox x:Name="nb" Value="{Binding Data, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}"/>
mm8's answer pointed me in the right direction.
The two key missing elements were indeed:
to change the TextBox in NumberBox.xaml such as
<TextBox Name="Container"
Text="{Binding Value, RelativeSource={RelativeSource AncestorType=UserControl}}"
HorizontalAlignment="Stretch" VerticalAlignment="Stretch" VerticalContentAlignment="Center"
/>
and to change the binding in MainWindow.xaml such as
...
<DataTemplate>
<BindingTest:NumberBox
Value="{Binding Data, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}"/>
</DataTemplate>
...
With these two modifications, both the Value and Data are being updated two-ways inside my DataGridTemplaceColumn.
Binding to the text however proved problematic. WPF apparently uses en-US culture no matter how your system is setup, which is really bad as this control deals with numbers. Using this trick solves that problem, and now the correct decimal separator is recognised. In my case, I've added it in a static constructor for the same effect so NumberBox can be used in other applications as is.
static NumberBox()
{
FrameworkElement.LanguageProperty.OverrideMetadata(
typeof(FrameworkElement),
new FrameworkPropertyMetadata(XmlLanguage.GetLanguage(CultureInfo.CurrentCulture.IetfLanguageTag)));
}
I've tested this in my test project, and then again tested the integration into my real project. As far as I can tell, it holds water, so I'll consider this question answered.
I am struggling to get a user control to accept a property from my Data Context object. I don't want to pass just the value; but the instance of the property because I would like to have converters operate on the attributes of the property.
I am very new to the WPF space, I've read many articles and none of them don't address this issue. The reason I'm trying to do this is because I have a calculations class that has many properties that need to be displayed and I don't really want to create a user control for each property or have 2,000 lines of repetitious XAML.
Any insight would be greatly appreciated.
Example Class
public class MyClass
{
[MyAttribute("someValue")]
public string Foo { get; set; }
}
View Model
public class MyViewModel : INotifyPropertyChanged
{
private _myClass;
public MyClass MyClass1
{
get => _myClass;
set
{
if(_myClass != value)
{
_myClass = value;
OnPropertyChanged();
}
}
}
}
Parent XAML
<UserControl DataContext="MyViewModel">
<Grid>
<!-- this is where I'm struggling, I think -->
<uc:MyConsumerControl ObjectProp="{Binding Path=MyClass1.Foo}"/>
</Grid>
</UserControl>
User Control
XAML
<UserControl DataContext={Binding RelativeSource={RelativeSource Mode=Self}}>
<Grid>
<TextBox Text="{Binding ObjectProp}"/>
<TextBlock Text="{Binding Path=ObjectProp, Converter={StaticResource MyAttrConverter}}"/>
</Grid>
</UserControl>
C#
public class MyConsumer : UserControl
{
public MyConsumer { InitializeComponent(); }
public object ObjectProp
{
get => (object)GetValue(ObjDepProp);
set => SetValue(ObjDepProp, value);
}
public static readonly DependencyProperty ObjDepProp =
DependencyProperty.Register(nameof(ObjectProp),
typeof(object), typeof(MyConsumer));
}
First of all, there is a naming convention for identifier fields of dependency properties:
public static readonly DependencyProperty ObjectPropProperty =
DependencyProperty.Register(nameof(ObjectProp), typeof(object), typeof(MyConsumer));
public object ObjectProp
{
get => GetValue(ObjectPropProperty);
set => SetValue(ObjectPropProperty, value);
}
Second, a UserControl that exposes bindable properties must never set its own DataContext, so this is wrong:
<UserControl DataContext={Binding RelativeSource={RelativeSource Mode=Self}}>
The XAML should look like this:
<UserControl ...>
<Grid>
<TextBox Text="{Binding ObjectProp,
RelativeSource={RelativeSource AncestorType=UserControl}}" />
<TextBlock Text="{Binding Path=ObjectProp,
RelativeSource={RelativeSource AncestorType=UserControl}, />
Converter={StaticResource MyAttrConverter}}"
</Grid>
</UserControl>
Finally, this is also wrong, because it only assigns a string to the DataContext:
<UserControl DataContext="MyViewModel">
It could probably look like shown below - although that would again explicitly set the DataContext of a UserControl, but perhaps one that could be considered a top-level view element like a Window or Page.
<UserControl ...>
<UserControl.DataContext>
<local:MyViewModel/>
</UserControl.DataContext>
<Grid>
<uc:MyConsumerControl ObjectProp={Binding Path=MyClass1.Foo}
</Grid>
</UserControl>
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);
}
}
}
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