WPF two way binding doesn't work until control is modified - c#

I'm trying to synchronize selection in a DataGrid using a collection in my data. I have this mostly working, with one little quirk.
When I change selection in the DataGrid changes are written to my data collection, so far so good. Then, if the data collection changes selection in my DataGrid is updated, as expected. However, if I modify my data before modifying the DataGrid then the DataGrid selection does not update.
An example of the first, working case
An example of the second, non-working case
Code
using System.Collections;
using System.Collections.ObjectModel;
using System.Windows;
using System.Windows.Controls;
namespace Testbed
{
public class Widget
{
public string Name { get; set; }
}
public class Data
{
public static Data Instance { get; } = new Data();
public ObservableCollection<Widget> Widgets { get; set; } = new ObservableCollection<Widget>();
public IList SelectedWidgets { get; set; } = new ObservableCollection<Widget>();
Data()
{
Widgets.Add(new Widget() { Name = "Widget 1" });
Widgets.Add(new Widget() { Name = "Widget 2" });
Widgets.Add(new Widget() { Name = "Widget 3" });
}
};
public class BindableDataGrid : DataGrid
{
public static readonly DependencyProperty SelectedItemsProperty = DependencyProperty.Register(
"SelectedItems",
typeof(IList),
typeof(BindableDataGrid),
new PropertyMetadata(default(IList)));
public new IList SelectedItems
{
get { return (IList) GetValue(SelectedItemsProperty); }
set { SetValue(SelectedItemsProperty, value); }
}
protected override void OnSelectionChanged(SelectionChangedEventArgs e)
{
base.OnSelectionChanged(e);
SetCurrentValue(SelectedItemsProperty, base.SelectedItems);
}
}
public partial class MainWindow : Window
{
public MainWindow ()
{
InitializeComponent();
}
private void Button1_Click(object sender, RoutedEventArgs e) { Button_Clicked(0); }
private void Button2_Click(object sender, RoutedEventArgs e) { Button_Clicked(1); }
private void Button3_Click(object sender, RoutedEventArgs e) { Button_Clicked(2); }
private void Button_Clicked(int index)
{
Data data = Data.Instance;
Widget widget = data.Widgets[index];
if (data.SelectedWidgets.Contains(widget))
{
data.SelectedWidgets.Remove(widget);
}
else
{
data.SelectedWidgets.Add(widget);
}
}
}
}
And markup
<Window
x:Class="Testbed.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:test="clr-namespace:Testbed"
Title="MainWindow"
Height="480" Width="640"
DataContext="{Binding Source={x:Static test:Data.Instance}}">
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition MinWidth="210" />
<ColumnDefinition Width="5" />
<ColumnDefinition MinWidth="210" />
<ColumnDefinition Width="5" />
<ColumnDefinition MinWidth="210" />
</Grid.ColumnDefinitions>
<!-- Change selection through data -->
<StackPanel Grid.Column="0">
<Button Content="Select Widget 1" Click="Button1_Click"/>
<Button Content="Select Widget 2" Click="Button2_Click"/>
<Button Content="Select Widget 3" Click="Button3_Click"/>
</StackPanel>
<!-- Current selection in data -->
<DataGrid Grid.Column="2"
ItemsSource="{Binding SelectedWidgets}"
IsReadOnly="true">
</DataGrid>
<!-- Change selection through UI -->
<test:BindableDataGrid Grid.Column="4"
SelectionMode="Extended"
ColumnWidth="*"
ItemsSource="{Binding Widgets}"
SelectedItems="{Binding SelectedWidgets, Mode=TwoWay}"
IsReadOnly="true">
<DataGrid.RowStyle>
<Style TargetType="{x:Type DataGridRow}">
<Style.Resources>
<SolidColorBrush x:Key="{x:Static SystemColors.InactiveSelectionHighlightBrushKey}" Color="CornflowerBlue"/>
</Style.Resources>
</Style>
</DataGrid.RowStyle>
</test:BindableDataGrid>
</Grid>
</Window>

The problem occurs because you do not handle notifications of the BindableDataGrid.SelectedItems collection.
In the first case you do not need to handle them manually because you actually get the SelectedItems collection from the base DataGrid class and pass it to the view model from the OnSelectionChanged method call. The base DataGrid handle notifications of this collection itself.
However, if you click the button first, the SelectedItems property get a new collection and the base DataGrid knows nothing about it.
I think that you need to handle the propertyChangedCallback, and handle notifications of provided collections to update selection in the grid manually. Refer to the following code demonstrating the concept. Note that I have renamed the property for simplicity but still have not debugged it.
public static readonly DependencyProperty SelectedItemsNewProperty = DependencyProperty.Register(
"SelectedItemsNew",
typeof(IList),
typeof(BindableDataGrid), new PropertyMetadata(OnPropertyChanged));
private static void OnPropertyChanged(DependencyObject d, DependencyPropertyChangedEventArgs e) {
BindableDataGrid bdg = (BindableDataGrid)d;
if (e.OldValue as INotifyCollectionChanged != null)
(e.NewValue as INotifyCollectionChanged).CollectionChanged -= bdg.BindableDataGrid_CollectionChanged;
if (Object.ReferenceEquals(e.NewValue, bdg.SelectedItems))
return;
if( e.NewValue as INotifyCollectionChanged != null )
(e.NewValue as INotifyCollectionChanged).CollectionChanged += bdg.BindableDataGrid_CollectionChanged;
bdg.SynchronizeSelection(e.NewValue as IList);
}
private void BindableDataGrid_CollectionChanged(object sender, NotifyCollectionChangedEventArgs e) {
SynchronizeSelection((IList)sender);
}
private void SynchronizeSelection( IList collection) {
SelectedItems.Clear();
if (collection != null)
foreach (var item in collection)
SelectedItems.Add(item);
}

This happens because your new SelectedItems property never updates the base SelectedItems when it is set. The problem with that is, of course, that MultiSelector.SelectedItems is readonly. It was designed specifically not to be set-able - but it was also designed to be updatable.
The reason your code works at all is because when you change the selection via the BindableDataGrid, SelectedWidgets gets replaced with the DataGrid's internal SelectedItemsCollection. After that point, you are adding and removing from that collection, so it updates the DataGrid.
Of course, this doesn't work if you haven't changed the selection yet, because OnSelectionChanged doesn't run until then, so SetCurrentValue is never called, so the binding never updated SelectedWidgets. But that's fine, all you have to do is called SetCurrentValue as part of BindableDataGrid's initialization.
Add this to BindableDataGrid:
protected override void OnInitialized(EventArgs e)
{
base.OnInitialized(e);
SetCurrentValue(SelectedItemsProperty, base.SelectedItems);
}
Be careful, though, because this will still break if you try to set SelectedItems sometime after initialization. It would be nice if you could make it readonly, but that prevents it from being used in data binding. So make sure that your binding uses OneWayToSource not TwoWay:
<test:BindableDataGrid Grid.Column="4"
SelectionMode="Extended"
ColumnWidth="*"
ItemsSource="{Binding Widgets}"
SelectedItems="{Binding SelectedWidgets, Mode=OneWayToSource}"
IsReadOnly="true">
<DataGrid.RowStyle>
<Style TargetType="{x:Type DataGridRow}">
<Style.Resources>
<SolidColorBrush x:Key="{x:Static SystemColors.InactiveSelectionHighlightBrushKey}" Color="CornflowerBlue"/>
</Style.Resources>
</Style>
</DataGrid.RowStyle>
</test:BindableDataGrid>
If you want to insure this never breaks, you can add a CoerceValueCallback to make sure the new SelectedItems is never set to something other than base.SelectedItems:
public static readonly DependencyProperty SelectedItemsProperty = DependencyProperty.Register(
"SelectedItems",
typeof(IList),
typeof(BindableDataGrid),
new PropertyMetadata(default(IList), null, (o, v) => ((BindableDataGrid)o).CoerceBindableSelectedItems(v)));
protected object CoerceBindableSelectedItems(object baseValue)
{
return base.SelectedItems;
}

#Drreamer's answer pointed me in the right direction. However, it boiled down to embracing the fact that the source data collection was being replaced by the DataGrid.SelectedItems collection. It ends up bypassing OnPropertyChanged after the first modification because both ends of the binding are actually the same object.
I didn't want the source collection to be replaced so I found another solution that synchronizes the contents of the collections. It has the benefit of being more direct as well.
When SelectedItems is initialized by the DependencyProperty I stash a reference to the source and target collections. I also register for CollectionChanged on the source and override OnSelectionChanged on the target. Whenever one collection changes I clear the other collection and copy the contents over. As another bonus I no longer have to expose my source collection as IList to allow the DependencyProperty to work since I'm not using it after caching off the source.
public class BindableDataGrid : DataGrid
{
public static readonly DependencyProperty SelectedItemsProperty = DependencyProperty.Register(
"SelectedItems",
typeof(IList),
typeof(BindableDataGrid),
new PropertyMetadata(OnPropertyChanged));
private static void OnPropertyChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
BindableDataGrid bdg = (BindableDataGrid) d;
if (bdg.initialized) return;
bdg.initialized = true;
bdg.source = (IList) e.NewValue;
bdg.target = ((DataGrid) bdg).SelectedItems;
((INotifyCollectionChanged) e.NewValue).CollectionChanged += bdg.OnCollectionChanged;
}
public new IList SelectedItems
{
get { return (IList) GetValue(SelectedItemsProperty); }
set { SetValue(SelectedItemsProperty, value); }
}
IList source;
IList target;
bool synchronizing;
bool initialized;
private void OnSourceChanged()
{
if (synchronizing) return;
synchronizing = true;
target.Clear();
foreach (var item in source)
target.Add(item);
synchronizing = false;
}
private void OnTargetChanged()
{
if (synchronizing) return;
synchronizing = true;
source.Clear();
foreach (var item in target)
source.Add(item);
synchronizing = false;
}
private void OnCollectionChanged(object sender, NotifyCollectionChangedEventArgs e)
{
OnSourceChanged();
}
protected override void OnSelectionChanged(SelectionChangedEventArgs e)
{
base.OnSelectionChanged(e);
OnTargetChanged();
}
}
I'm sure there's is a much more elegant way to solve this, but this is the best I've got right now.

Related

IEnumerable changed event

I am programming an app in WinUI 3 using C#.
There are some controls that have an ItemsSource property.
When I pass an IEnumerable into the ItemsSource and then change the IEnumerable, the control changes accordingly.
I now want to implement the same behavior in my custom UserControl.
I have the following code, but it only works with IEnumerables that implement INotifyCollectionChanged (eg. ObservableCollection).
WinUI supports IEnumerables that don't implement INotifyCollectionChanged.
How can I do the same?
Here is my code:
public sealed partial class MyUserControl : UserControl
{
public static readonly DependencyProperty ItemsSourceProperty =
DependencyProperty.Register("ItemsSource", typeof(IEnumerable), typeof(MyUserControl), new PropertyMetadata(null));
public IEnumerable ItemsSource
{
get { return (IEnumerable)GetValue(ItemsSourceProperty); }
set { SetValue(ItemsSourceProperty, value); }
}
private IEnumerable _oldItemsSource;
public MyUserControl()
{
this.InitializeComponent();
RegisterPropertyChangedCallback(ItemsSourceProperty, OnItemsSourceChanged);
}
private void OnItemsSourceChanged(DependencyObject sender, DependencyProperty prop)
{
if (prop == ItemsSourceProperty)
{
var newValue = (IEnumerable)sender.GetValue(ItemsSourceProperty);
if (_oldItemsSource is INotifyCollectionChanged oldCollection)
{
oldCollection.CollectionChanged -= OnCollectionChanged;
}
if (newValue is INotifyCollectionChanged collection)
{
collection.CollectionChanged += OnCollectionChanged;
}
_oldItemsSource = ItemsSource;
}
}
private void OnCollectionChanged(object sender, NotifyCollectionChangedEventArgs e)
{
// Update the control here
}
}
WinUI 3 allows me to use a List (doesn't implement INotifyCollectionChanged) as an ItemsSource.
Changes made to the List affect the control.
This is the code inside a test page:
public TestPage()
{
this.InitializeComponent();
var list = new List<string> { "Item1", "Item2", "Item3" };
var bar = new BreadcrumbBar(); ;
bar.ItemsSource = list;
this.Content = bar;
list.Add("Item4");
// The BreadcrumbBar now has 4 elements.
}
When I pass an IEnumerable into the ItemsSource and then change the IEnumerable, the control changes accordingly.
No, it doesn't unless the IEnumerable is an INotifyCollectionChanged.
Try to call list.Add("Item4") in an event handler after the control has been initially rendered if you don't believe me.
For example, this code will not add "Item4" to the BreadcrumbBar control:
public sealed partial class TestPage : Page
{
private readonly List<string> list = new List<string> { "Item1", "Item2", "Item3" };
public TestPage()
{
this.InitializeComponent();
breadcrumbBar.ItemsSource = list;
}
private void Button_Click(object sender, RoutedEventArgs e)
{
list.Add("Item4");
}
}
Changing the type of list to ObservableCollection<string> will make it work as expected.
So your custom control is no worse than any built-in control in that sense. The source collection must notify the view somehow.
WinUI 3 allows me to use a List (doesn't implement INotifyCollectionChanged) as an ItemsSource. Changes made to the List affect the control.
It most certainly doesn't react to changes of "dumb" collections. Your example only works because the items haven't been constructed yet, and they won't be until the control is loaded (your code is all in the constructor).
This example will show you a comparison between a plain List and an ObservableCollection.
Let's say we have a UserControl like this one.
TestUserControl.xaml
<UserControl
x:Class="ItemsSourceTest.TestUserControl"
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:local="using:ItemsSourceTest"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
mc:Ignorable="d">
<Grid>
<ListView ItemsSource="{x:Bind ItemsSource, Mode=OneWay}" />
</Grid>
</UserControl>
TestUserControl.xaml.cs
using Microsoft.UI.Xaml;
using Microsoft.UI.Xaml.Controls;
using Microsoft.UI.Xaml.Data;
using System.Collections;
using Windows.Foundation.Collections;
namespace ItemsSourceTest;
public sealed partial class TestUserControl : UserControl
{
public static readonly DependencyProperty ItemsSourceProperty = DependencyProperty.Register(
nameof(ItemsSource),
typeof(IEnumerable),
typeof(TestUserControl),
new PropertyMetadata(default, (d, e) => (d as TestUserControl)?.UpdateItemsSource()));
public TestUserControl()
{
this.InitializeComponent();
}
// You don't need CollectionViewSource or CollectionView
// in order to populate the ListView. This is just to show
// you how you can get events when the collection changes.
public CollectionViewSource? CollectionViewSource { get; set; }
public ICollectionView? CollectionView { get; set; }
public IEnumerable ItemsSource
{
get => (IEnumerable)GetValue(ItemsSourceProperty);
set => SetValue(ItemsSourceProperty, value);
}
private void UpdateItemsSource()
{
CollectionViewSource = new CollectionViewSource()
{
Source = ItemsSource,
};
if (CollectionView is not null)
{
CollectionView.VectorChanged -= CollectionView_VectorChanged;
}
CollectionView = CollectionViewSource.View;
CollectionView.VectorChanged += CollectionView_VectorChanged;
}
private void CollectionView_VectorChanged(IObservableVector<object> sender, IVectorChangedEventArgs #event)
{
// Your can do your work for collection changes here...
}
}
And a MainPage a ListView and a TestUserControl both bound to a plain List named NonObservableItems and another set of a ListView and a TestUserControl both bound to an ObservableCollection named ObservableItems.
When you add items to the collections, you'll see that only the controls bound to the ObservableItems will be populated.
MainPage.xaml
<Page
x:Class="ItemsSourceTest.MainPage"
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:local="using:ItemsSourceTest"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
Background="{ThemeResource ApplicationPageBackgroundThemeBrush}"
mc:Ignorable="d">
<Grid RowDefinitions="Auto,*">
<Button
Grid.Row="0"
Click="Button_Click"
Content="Add item" />
<Grid
Grid.Row="1"
ColumnDefinitions="*,*"
RowDefinitions="Auto,*,*">
<TextBlock
Grid.Row="0"
Grid.Column="0"
Text="Non-Observable Items" />
<ListView
Grid.Row="1"
Grid.Column="0"
ItemsSource="{x:Bind NonObservableItems, Mode=OneWay}" />
<local:TestUserControl
Grid.Row="2"
Grid.Column="0"
ItemsSource="{x:Bind NonObservableItems, Mode=OneWay}" />
<TextBlock
Grid.Row="0"
Grid.Column="1"
Text="Observable Items" />
<ListView
Grid.Row="1"
Grid.Column="1"
ItemsSource="{x:Bind ObservableItems, Mode=OneWay}" />
<local:TestUserControl
Grid.Row="2"
Grid.Column="1"
ItemsSource="{x:Bind ObservableItems, Mode=OneWay}" />
</Grid>
</Grid>
</Page>
MainPage.xaml.cs
using Microsoft.UI.Xaml;
using Microsoft.UI.Xaml.Controls;
using System.Collections.Generic;
using System.Collections.ObjectModel;
namespace ItemsSourceTest;
public sealed partial class MainPage : Page
{
public MainPage()
{
this.InitializeComponent();
AddItem();
}
public List<string> NonObservableItems { get; set; } = new();
public ObservableCollection<string> ObservableItems { get; set; } = new();
private int Counter { get; set; } = 0;
private void AddItem()
{
NonObservableItems.Add(Counter.ToString());
ObservableItems.Add(Counter.ToString());
Counter++;
}
private void Button_Click(object sender, RoutedEventArgs e)
{
AddItem();
}
}

Binding the Itemsource of a Listbox to a method in Code behind

I have a Database and a XAML File an in there I have two Listboxes, the first one (lbAg) to list the topics (ag_name, like finances, HR, etc.) and the second one (liAn) to list the tasks in that field which have to be done (an_anligen, like paying the accountant etc..).
The thing is, I want the Items of liAn to be the tasks of the selected Item from liAg, but only if the attribute an_fertig (an_done) to be false. This I want to achieve by binding it to a method in the code behind.
Please help,
Thanks in advance.
XAML:
<ListBox Grid.Column="0"
Grid.RowSpan="2"
x:Name="liAg"
Loaded="liAg_Loaded"
DisplayMemberPath="ag_name" />
<ListBox Grid.Column="1"
Grid.RowSpan="2"
x:Name="liAn"
ItemsSource="{Binding liAn_Items}"
DisplayMemberPath="an_titel" />
Code-behind:
public partial class Main : UserControl
{
public Main()
{
InitializeComponent();
}
DbEntities db = new DbEntities();
private void liAg_Loaded(object sender, RoutedEventArgs e)
{
liAg.ItemsSource = db.ag_arbeitsgemeinschaft.ToList();
}
private void liAn_Items(object sender, RoutedEventArgs e)
{
string liag = liAg.SelectedItem.ToString();
Console.WriteLine(liag);
var erg = from a in db.an_anliegen
where a.an_ag == liag && a.an_fertig != true
select a;
liAn.ItemsSource = erg.ToList();
}
}
If you really want to bind to a method, you can use the ObjectDataProvider: Microsoft Docs: How to: Bind to a Method.
You have many options. You can make use of the ListBox.SelectionChanged event to trigger the database query.
I highly recommend not to mix XAML data binding and direct property assignment using C#. You should use data binding when possible.
I have created some DependencyProperty data source properties for the ListBox.ItemsSource properties to bind to. Since you didn't supplied any details about your data item type, I bound the ListBox to a collection of the fictional DataItem type:
MainWindow.xaml.cs
partial class MainWindow : Window
{
public static readonly DependencyProperty DataItemsProperty = DependencyProperty.Register(
"DataItems",
typeof(IEnumerable),
typeof(MainWindow),
new PropertyMetadata(default(IEnumerable)));
public IEnumerable DataItems
{
get => (IEnumerable) GetValue(MainWindow.DataItemsProperty);
set => SetValue(MainWindow.DataItemsProperty, value);
}
public static readonly DependencyProperty TasksProperty = DependencyProperty.Register(
"Tasks",
typeof(IEnumerable),
typeof(MainWindow),
new PropertyMetadata(default(IEnumerable)));
public IEnumerable Tasks
{
get => (IEnumerable) GetValue(MainWindow.TasksProperty);
set => SetValue(MainWindow.TasksProperty, value);
}
public MainWindow()
{
this.DataItems = db.ag_arbeitsgemeinschaft.ToList();
}
private void OnSelectionChanged(object sender, SelectionChangedEventArgs e)
{
// Cast the ListBox.SelectedItem from object to the data type
DataItem selectedItem = e.AddedItems.OfType<DataItem>().FirstOrDefault();
if (selectedItem == null)
{
return;
}
// Access the selected item's members
string displayedValue = selectedItem.ag_name;
// Execute query
var erg = from a in db.an_anliegen
where a.an_ag == displayedValue && !a.an_fertig
select a;
// Update binding source of the ListBox named 'liAn'
this.Tasks = erg.ToList();
}
}
MainWindow.xaml
<Window>
<ListBox x:Name="liAg"
ItemsSource="{Binding RelativeSource={RelativeSource AncestorType=MainWindow},
Path=DataItems}"
DisplayMemberPath="ag_name"
SelectionChanged="OnSelectionChanged" />
<ListBox x:Name="liAn"
ItemsSource="{Binding RelativeSource={RelativeSource AncestorType=MainWindow},
Path=Tasks}"
DisplayMemberPath="an_titel" />
</Window>

WPF UserControl with nullable dependency property dependent on checkbox value

I'm trying to create a custom UserControl that displays the properties of a complex object as a form. Additionally, if the user unchecks a checkbox in the header of the UserControl, the value of the dependency property should be null (but the form values should stay displayed, even if the form is disabled) - and vice versa.
I'm working with two dependency properties of type ComplexObject on the UserControl - one is public (this will be bound to by the client), another is private (its properties will be bound to the internal controls in the UserControl):
public ComplexObject ComplexObject
{
get { return (ComplexObject )GetValue(ComplexObjectProperty); }
set { SetValue(ComplexObjectProperty, value); }
}
private ComplexObject VisibleComplexObject
{
get { return (ComplexObject)GetValue(VisibleComplexObjectProperty); }
set { SetValue(VisibleComplexObjectProperty, value); }
}
Now I'm struggling with a binding between those two, so that CompexObject becomes either VisibleComplexObject or null based on the checkbox value. This should also work the other way. I've tried to solve this using DataTriggers in the Style of the UserControl, but was unable to do so:
<UserControl.Style>
<Style TargetType="local:CheckableComplexTypeGroup">
// 'CheckableComplexTypeGroup' TargetType does not match type of the element 'UserControl'
</Style>
</UserControl.Style>
Using <local:CheckableComplexTypeGroup.Style> instead of <UserControl.Style> didn't work either.
Are there any other suggestions? Or maybe another way of doing this?
Finally I've solved this without using plain old event handlers instead of binding/triggers.
CheckableComplexObjectGroup.xaml:
<UserControl Name="thisUC" ...>
<GroupBox>
<GroupBox.Header>
<CheckBox Name="cbComplexObject"
IsChecked="{Binding ElementName=thisUC, Path=ComplexObject, Mode=OneWay, Converter={StaticResource nullToFalseConv}}"
Checked="cbComplexObject_Checked" Unchecked="cbComplexObject_Unchecked"/>
</GroupBox.Header>
...
</UserControl>
CheckableComplexObjectGroup.cs:
public static readonly DependencyProperty ComplexObjectProperty =
DependencyProperty.Register("ComplexObject",
typeof(ComplexObject),
typeof(CheckableComplexObjectGroup),
new PropertyMetadata(null));
private static readonly DependencyProperty VisibleComplexObjectProperty =
DependencyProperty.Register("VisibleComplexObject",
typeof(ComplexObject),
typeof(CheckableComplexObjectGroup),
new PropertyMetadata(new ComplexObject()));
//...
protected override void OnPropertyChanged(DependencyPropertyChangedEventArgs e)
{
if (e.Property == ComplexObjectProperty)
{
if (null != e.NewValue)
VisibleComplexObject = ComplexObject;
}
base.OnPropertyChanged(e);
}
private void cbComplexObject_Checked(object sender, RoutedEventArgs e)
{
ComplexObject = VisibleComplexObject;
}
private void cbComplexObject_Unchecked(object sender, RoutedEventArgs e)
{
ComplexObject = null;
}

How to change the itemsSource of a ComboBox in C#

I am trying to change the itemsSource of a comboBox at run-time. In this question I was told to do, comboBox.itemssource.... That would be okay if all I needed to do was create a new comboBox and then call the command on it. However, I need to perform this operation on a comboBox that already exists in my User Control through xaml. In that case, how would I reference it? I know how to bind to properties in the control, but in this case I would need to get the whole control. Am I over-thinking it? What is the best way to do what I'm thinking?
This how I am currently switching the Collections in the comboBox (This is all at the model level):
//Property for Combo Box List
public ObservableCollection<string> ComboBoxList
{
get { return _comboBoxList; }
set
{
if (Equals(value, _comboBoxList)) return;
_comboBoxList = value;
OnPropertyChanged("ComboBoxList");
}
}
public string SelectedCommand
{
get { return _selectedCommand; }
set
{
_selectedCommand = value;
NotifyPropertyChange(() => SelectedCommand);
if (SelectedCommand == "String Value")
{
ComboBoxList = new ObservableCollection<string>(newList);
}
}
}
The collections switch when using this implementation, but the selectedItem in the comboBox doesn't stick. For example, when I click on a different command and then switch back, the box no longer has a selectedItem.
UPDATE
I have a property called selectedOperation that is bound to my comboBox. It contains a simple getter and setter, with a NotifyPropertyChange. This makes it so that the selectedItem in the box stays selected. BUT, if the user clicks on a different command and selects a different item in the comboBox, that new item takes it's place. I need to be able to have a selectedItem for each collection that the comboBox holds.
For example:
Let's say there are 2 commands in the listBox, A and B. Each create a different collection in the comboBox. A creates a collection of numbers, and B creates a collection of names.
For command A the user selects 5. When A is selected the comboBox should display 5 as it's selectedItem. A -> 5
For command B the user selectes Roger. When B is selected the comboBox should display "Roger" as it's selectedItem. B -> Roger
Currently, the comboBox does not remember it's selectedItem when the user switches between commands.
I would rather use a DataContext and update that source than manually updating a ComboBox.ItemsSourceproperty.
This way there would be no need to know about the controls at all.
Here is a small example :
When the user clicks the button, you just take care of updating your data, not the controls presenting it.
<Window x:Class="WpfApplication10.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" x:Name="Window1">
<Grid DataContext="{Binding ElementName=Window1}">
<StackPanel>
<Button Click="Button_Click">Some data 1</Button>
<Button Click="Button_Click_1">Some data 2</Button>
<ListBox x:Name="ComboBox1" ItemsSource="{Binding Collection}"></ListBox>
</StackPanel>
</Grid>
</Window>
using System.Collections.ObjectModel;
using System.Windows;
namespace WpfApplication10
{
/// <summary>
/// Interaction logic for MainWindow.xaml
/// </summary>
public partial class MainWindow : Window
{
private readonly ObservableCollection<string> _collection = new ObservableCollection<string>();
public MainWindow()
{
InitializeComponent();
}
public ObservableCollection<string> Collection
{
get { return _collection; }
}
private void Button_Click(object sender, RoutedEventArgs e)
{
_collection.Clear();
for (int i = 0; i < 5; i++)
{
_collection.Add("method 1 item " + i);
}
}
private void Button_Click_1(object sender, RoutedEventArgs e)
{ _collection.Clear();
for (int i = 0; i < 5; i++)
{
_collection.Add("method 2 item " + i);
}
}
}
}
Update
If you want to use a new collection instead of removing items, you will have to implement INotifyPropertyChanged for the collection.
using System.Collections.ObjectModel;
using System.ComponentModel;
using System.Runtime.CompilerServices;
using System.Windows;
namespace WpfApplication10
{
/// <summary>
/// Interaction logic for MainWindow.xaml
/// </summary>
public partial class MainWindow : Window, INotifyPropertyChanged
{
private ObservableCollection<string> _collection = new ObservableCollection<string>();
public MainWindow()
{
InitializeComponent();
}
public ObservableCollection<string> Collection
{
get { return _collection; }
set
{
if (Equals(value, _collection)) return;
_collection = value;
OnPropertyChanged();
}
}
public event PropertyChangedEventHandler PropertyChanged;
private void Button_Click(object sender, RoutedEventArgs e)
{
Collection = new ObservableCollection<string>(new[] {"1", "2"});
}
private void Button_Click_1(object sender, RoutedEventArgs e)
{
Collection = new ObservableCollection<string>(new[] {"3", "4"});
}
protected virtual void OnPropertyChanged([CallerMemberName] string propertyName = null)
{
PropertyChangedEventHandler handler = PropertyChanged;
if (handler != null) handler(this, new PropertyChangedEventArgs(propertyName));
}
}
}
Note: the [CallerMemberName] saves you from adding the property name each time you invoke the invocator but it's only for .NET 4.5 if I remember correctly.
If you are not under .NET 4.5 then you'll have to put OnPropertyChanged("Collection") instead.
Reference : INotifyPropertyChanged
Also, update Collection with a new collection, not _collection otherwise your UI won't be notified.
EDIT 2
You need to track the selected item according the collection used.
<Window x:Class="WpfApplication10.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" x:Name="Window1">
<Grid>
<StackPanel>
<Button Click="Button_Click">Some data 1</Button>
<Button Click="Button_Click_1">Some data 2</Button>
<ListBox x:Name="ComboBox1" ItemsSource="{Binding}" SelectedItem="{Binding MySelectedItem}" />
</StackPanel>
</Grid>
</Window>
Code behind :
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.ComponentModel;
using System.Windows;
namespace WpfApplication10
{
public partial class MainWindow
{
public MainWindow()
{
InitializeComponent();
MyCustomCollection1 = new MyCustomCollection<string>(new[] {"a", "b"});
MyCustomCollection2 = new MyCustomCollection<string>(new[] {"c", "d"});
}
public MyCustomCollection<string> MyCustomCollection1 { get; set; }
public MyCustomCollection<string> MyCustomCollection2 { get; set; }
private void Button_Click(object sender, RoutedEventArgs e)
{
DataContext = MyCustomCollection1;
}
private void Button_Click_1(object sender, RoutedEventArgs e)
{
DataContext = MyCustomCollection2;
}
}
public class MyCustomCollection<T> : ObservableCollection<T>
{
private T _mySelectedItem;
public MyCustomCollection(IEnumerable<T> collection) : base(collection)
{
}
public T MySelectedItem
{
get { return _mySelectedItem; }
set
{
if (Equals(value, _mySelectedItem))return;
_mySelectedItem = value;
OnPropertyChanged(new PropertyChangedEventArgs("MySelectedItem"));
}
}
}
}
try changing the collection via style using some trigger (can be any trigger data/event) here is an example:
<Style x:Key="MySelectItemSourceStyle" TargetType="ComboBox">
<Setter Property="ItemsSource" Value="{Binding Collection1}" />
<Style.Triggers>
<DataTrigger Binding="{Binding SomeValue}" Value="SecondCollection">
<Setter Property="ItemsSource" Value="{Binding Collection2}" />
</DataTrigger>
</Style.Triggers>
</Style>

Iterating selected items in Windows App Store Gridview

I know this is a long one, but please bear with me.
I have created a windows app store program very similar to Laurent Bugnion's "MyFriends" program in the MVVM light samples using the MVVM light framework.
In his program he uses the SelectedItem property of the gridview to keep track of which item is the selected item.
The problem is, I give the user the ability to select multiple items on the GridView and then operate on them using a button on the App Bar. For this SelectedItem will not work.
Does anyone know how to make this work with a multiselect GridView? I have tried the IsSelected property of the GridViewItem based on some articles on WPF, but this doesn't seem to work. The SelectedTimesheets getter always come back empty when called. Here is what I have so far:
MainPage.xaml (bound to a MainViewModel with a child TimesheetViewModel observable collection):
<GridView
x:Name="itemGridView"
IsItemClickEnabled="True"
ItemsSource="{Binding Timesheets}"
ItemTemplate="{StaticResource TimesheetTemplate}"
Margin="10"
Grid.Column="0"
SelectionMode="Multiple"
helpers:ItemClickCommand.Command="{Binding NavigateTimesheetCommand}" RenderTransformOrigin="0.738,0.55" >
<GridView.ItemContainerStyle>
<Style TargetType="GridViewItem">
<Setter Property="IsSelected" Value="{Binding IsSelected, Mode=TwoWay}"/>
</Style>
</GridView.ItemContainerStyle>
</GridView>
MainViewModel (cut down from full code):
public class MainViewModel : ViewModelBase
{
private readonly IDataService _dataService;
private readonly INavigationService _navigationService;
/// <summary>
/// Initializes a new instance of the MainViewModel class.
/// </summary>
public MainViewModel(IDataService dataService, INavigationService navigationService)
{
_dataService = dataService;
_navigationService = navigationService;
Timesheets = new ObservableCollection<TimesheetViewModel>();
ExecuteRefreshCommand();
}
public ObservableCollection<TimesheetViewModel> Timesheets
{
get;
private set;
}
public IEnumerable<TimesheetViewModel> SelectedTimesheets
{
get { return Timesheets.Where(o => o.IsSelected); }
}
private async void ExecuteRefreshCommand()
{
var timesheets = await _dataService.GetTimesheets("domain\\user");
if (timesheets != null)
{
Timesheets.Clear();
foreach (var timesheet in timesheets)
{
Timesheets.Add(new TimesheetViewModel(timesheet));
}
}
}
}
TimesheetViewModel:
public class TimesheetViewModel: ViewModelBase
{
public bool IsSelected { get; set; }
public Timesheet Model
{
get;
private set;
}
public TimesheetViewModel(Timesheet model)
{
Model = model;
}
}
If I set the IsSelected property manually, the SelectedTimesheets lambda works, so the problem is somewhere in the binding of the XAML to the IsSelected property.
Any help would be appreciated.
Sure, I know what you mean. Too bad this isn't automagic, but it isn't. The solution involves a simple custom GridView that inherits from GridView. Nothing too crazy, that is, if you let it sink in. Here's the code, I just tested it:
Here's your XAML:
<Grid Background="{StaticResource ApplicationPageBackgroundThemeBrush}">
<Grid.ColumnDefinitions >
<ColumnDefinition />
<ColumnDefinition />
</Grid.ColumnDefinitions>
<local:MyGridView ItemsSource="{Binding Items}" SelectionMode="Multiple"
BindableSelectedItems="{Binding Selected}" />
<local:MyGridView Grid.Column="1" ItemsSource="{Binding Selected}" />
</Grid>
Here's your view model (super-simplified):
public class ViewModel
{
ObservableCollection<string> m_Items
= new ObservableCollection<string>(Enumerable.Range(1, 100).Select(x => x.ToString()));
public ObservableCollection<string> Items { get { return m_Items; } }
ObservableCollection<object> m_Selected = new ObservableCollection<object>();
public ObservableCollection<object> Selected { get { return m_Selected; } }
}
And here's your custom gridview:
public class MyGridView : GridView
{
public ObservableCollection<object> BindableSelectedItems
{
get { return GetValue(BindableSelectedItemsProperty) as ObservableCollection<object>; }
set { SetValue(BindableSelectedItemsProperty, value as ObservableCollection<object>); }
}
public static readonly DependencyProperty BindableSelectedItemsProperty =
DependencyProperty.Register("BindableSelectedItems",
typeof(ObservableCollection<object>), typeof(MyGridView),
new PropertyMetadata(null, (s, e) =>
{
(s as MyGridView).SelectionChanged -= (s as MyGridView).MyGridView_SelectionChanged;
(s as MyGridView).SelectionChanged += (s as MyGridView).MyGridView_SelectionChanged;
}));
void MyGridView_SelectionChanged(object sender, SelectionChangedEventArgs e)
{
if (BindableSelectedItems == null)
return;
foreach (var item in BindableSelectedItems.Where(x => !this.SelectedItems.Contains(x)).ToArray())
BindableSelectedItems.Remove(item);
foreach (var item in this.SelectedItems.Where(x => !BindableSelectedItems.Contains(x)))
BindableSelectedItems.Add(item);
}
}
Just one new property BindableSelectedItems.
Best of luck!
#Jerry-Nixon-MSFT's answer spurred me on to rethink it (thanks to him) and I came up with the following solution.
Firstly I changed the XAML to accept a new helper method SelectionChangedCommand.Command and bound it to a RelayCommand called SelectionChangedCommand in my view model
MainPage.xaml
<GridView
x:Name="itemGridView"
IsItemClickEnabled="True"
ItemsSource="{Binding Timesheets}"
ItemTemplate="{StaticResource TimesheetTemplate}"
Margin="10"
Grid.Column="0"
SelectionMode="Multiple"
helpers:ItemClickCommand.Command="{Binding NavigateTimesheetCommand}"
helpers:SelectionChangedCommand.Command="{Binding SelectionChangedCommand}
"RenderTransformOrigin="0.738,0.55" >
</GridView>
I then added a SelectionChangedCommand helper class under my helpers namespace to translate the SelectionChanged event into an ICommand
namespace TimesheetManager.Helpers
{
public class SelectionChangedCommand
{
public static readonly DependencyProperty CommandProperty =
DependencyProperty.RegisterAttached("Command", typeof(ICommand),
typeof(SelectionChangedCommand), new PropertyMetadata(null,
OnCommandPropertyChanged));
public static void SetCommand(DependencyObject d, ICommand value)
{
d.SetValue(CommandProperty, value);
}
public static ICommand GetCommand(DependencyObject d)
{
return (ICommand)d.GetValue(CommandProperty);
}
private static void OnCommandPropertyChanged(DependencyObject d,
DependencyPropertyChangedEventArgs e)
{
var control = d as ListViewBase;
if (control != null)
control.SelectionChanged += OnSelectionChanged;
}
private static void OnSelectionChanged(object sender, SelectionChangedEventArgs e)
{
var control = sender as ListViewBase;
var command = GetCommand(control);
if (command != null && command.CanExecute(e))
command.Execute(e);
}
}
}
This binds the SelectionChanged event of any control which inherits from ListViewBase (our gridview) to a method called OnSelectionChanged. OnSelectionChanged subsequently passes the SelectionChangedEventArgs from the control to the RelayCommand binding in the XAML.
Finally in MainViewModel, I process the RelayCommand and set the IsSelected flag:
MainViewModel:
private RelayCommand<object> _selectionChangedCommand;
/// <summary>
/// Gets the SelectionChangedCommand.
/// </summary>
public RelayCommand<object> SelectionChangedCommand
{
get
{
return _selectionChangedCommand ?? (_selectionChangedCommand = new RelayCommand<object>
((param) => ExecuteSelectionChangedCommand(param)));
}
}
private void ExecuteSelectionChangedCommand(object sender)
{
var x = sender as SelectionChangedEventArgs;
foreach (var item in x.AddedItems)
((TimesheetViewModel)item).IsSelected = true;
foreach (var item in x.RemovedItems)
((TimesheetViewModel)item).IsSelected = false;
}
I know there is a fair amount of casting going on, but we are limited to object by the ICommand interface.
Hope this helps.

Categories

Resources