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();
}
}
Related
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.
I'm binding a ListView to an ICollectionView in my viewmodel. The ICollectionView has some predefined filters that are applied when you click some buttons. However I cannot seem to find any way to (auto) select the first item in the ListView after the collection has been filtered.
I've tried to set SelectedIndex=0, add both Target and Source notification to the binding, but all are ineffective when the filter applies.
Any pointers on how to achieve this?
EDIT: Below code illustrates my issue I'd say.
XAML:
<Window x:Class="CollectionViewTest.MainWindow"
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:local="clr-namespace:CollectionViewTest"
mc:Ignorable="d"
Title="MainWindow" Height="450" Width="800">
<Window.DataContext>
<local:MainViewModel/>
</Window.DataContext>
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition />
<ColumnDefinition />
<ColumnDefinition />
</Grid.ColumnDefinitions>
<!-- MENU -->
<StackPanel Orientation="Vertical">
<Button Content="Numbers below 4" Click="Below4_Click" Width="100"/>
<Button Content="Numbers below 7" Click="Below7_Click" Width="100"/>
<Button Content="All numbers" Click="All_Click" Width="100"/>
</StackPanel>
<!-- LIST -->
<ListView
Grid.Column="1"
SelectedIndex="0"
ItemsSource="{Binding Numbers, Mode=OneWay}"
SelectedItem="{Binding SelectedNumber, Mode=TwoWay}">
<ListView.Resources>
<DataTemplate DataType="{x:Type local:Number}">
<TextBlock Text="{Binding Value}" />
</DataTemplate>
</ListView.Resources>
</ListView>
<!-- DETAILS -->
<TextBlock Grid.Column="2" Text="{Binding SelectedNumber.Text}" Width="100"/>
</Grid>
</Window>
Code-Behind:
using System.Windows;
namespace CollectionViewTest
{
public partial class MainWindow : Window
{
private MainViewModel vm;
public MainWindow()
{
InitializeComponent();
vm = (MainViewModel)DataContext;
}
private void Below4_Click(object sender, RoutedEventArgs e)
{
vm.MenuFilter = f => f.Value < 4;
}
private void Below7_Click(object sender, RoutedEventArgs e)
{
vm.MenuFilter = f => f.Value < 7;
}
private void All_Click(object sender, RoutedEventArgs e)
{
vm.MenuFilter = f => true;
}
}
}
ViewModel:
using System;
using System.ComponentModel;
using System.Runtime.CompilerServices;
using System.Windows.Data;
using System.Collections.ObjectModel;
namespace CollectionViewTest
{
public class MainViewModel : PropertyChangedBase
{
public MainViewModel()
{
Numbers = new ObservableCollection<Number>();
NumberCollection = CollectionViewSource.GetDefaultView(Numbers);
NumberCollection.Filter = Filter;
NumberCollection.SortDescriptions.Add(new SortDescription("Value", ListSortDirection.Ascending));
for (int i = 0; i < 10; i++)
Numbers.Add(new Number { Value = i, Text = $"This is number {i}." });
}
private Func<Number, bool> menuFilter;
public Func<Number, bool> MenuFilter
{
get => menuFilter;
set
{
menuFilter = value;
NumberCollection.Refresh();
}
}
private bool Filter(object item)
{
var number = (Number)item;
return MenuFilter == null ? true : MenuFilter(number);
}
public ObservableCollection<Number> Numbers { get; set; }
public ICollectionView NumberCollection { get; set; }
private Number selectedNumber;
public Number SelectedNumber { get => selectedNumber; set => Set(ref selectedNumber, value); }
}
public class Number : PropertyChangedBase
{
public int Value { get; set; }
private string text;
public string Text { get => text; set => Set(ref text, value); }
}
public class PropertyChangedBase : INotifyPropertyChanged
{
public event PropertyChangedEventHandler PropertyChanged;
protected void Set<T>(ref T field, T newValue = default(T), [CallerMemberName] string propertyName = null)
{
field = newValue;
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
}
}
As you can see, pressing one of the buttons changes the Filter and calls Refresh on the collection. What I would like to have, is that the first item in the list (here '0') is selected automatically which then would display the text "This is number 0" in the text in column 2.
I have tried both the SelectedIndex=0 and also MoveCurrentToFirst but nothing is selected.
Don't set SelectedIndex when binding to an ICollectionView. Instead, set its CurrentItem via MoveCurrentTo() or MoveCurrentToFirst():
myCollectionView.MoveCurrentTo(someItem);
...
myCollectionView.MoveCurrentToFirst();
Also, set IsSynchronizedWithCurrentItem on your ListView:
<ListView IsSynchronizedWithCurrentItem="True" ...
Detect when filter is applied
When the filter is evaluated, the collection view is refreshed which in turn resets the collection. To detect this, listen for the CollectionChanged event and look for the NotifyCollectionChangedAction.Reset flag. Please refer to the CollectionView source code for more details.
I'm trying to bind a user control property "MyUserControl.Names" to a collection property "Names" of the main window. It doesn't work if I do it in ItemsControl template, but it works if I move the control definition out of the ItemsControl template. Here is the xaml:
<Window x:Class="TestItemsControl.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="clr-namespace:TestItemsControl"
Height="200" Width="200"
Name="MainControl">
<StackPanel>
<ItemsControl ItemsSource="{Binding Groups, ElementName=MainControl}">
<ItemsControl.ItemTemplate>
<DataTemplate>
<!-- This doesn't work -->
<StackPanel Background="WhiteSmoke" Height="40" Width="100" Margin="5" HorizontalAlignment="Left">
<TextBlock Text="{Binding .}"/>
<local:MyUserControl Names="{Binding Names, ElementName=MainControl}"/>
</StackPanel>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
<!-- This works -->
<StackPanel Background="WhiteSmoke" Height="40" Width="100" Margin="5" HorizontalAlignment="Left">
<TextBlock Text="Group3"/>
<local:MyUserControl Names="{Binding Names, ElementName=MainControl}"/>
</StackPanel>
</StackPanel>
</Window>
MainWindow.xaml.cs contains two dependency properties:
public partial class MainWindow : Window
{
public MainWindow()
{
InitializeComponent();
SetValue(GroupsProperty, new ObservableCollection<string>());
SetValue(NamesProperty, new ObservableCollection<string>());
Groups.Add("Group1");
Groups.Add("Group2");
Names.Add("Name1");
Names.Add("Name2");
}
public static readonly DependencyProperty GroupsProperty =
DependencyProperty.Register("Groups", typeof(ObservableCollection<string>), typeof(MainWindow),
new PropertyMetadata(null));
public ObservableCollection<string> Groups
{
get { return (ObservableCollection<string>)GetValue(GroupsProperty); }
set { SetValue(GroupsProperty, value); }
}
public static readonly DependencyProperty NamesProperty =
DependencyProperty.Register("Names", typeof(ObservableCollection<string>), typeof(MainWindow),
new PropertyMetadata(null));
public ObservableCollection<string> Names
{
get { return (ObservableCollection<string>)GetValue(NamesProperty); }
set { SetValue(NamesProperty, value); }
}
}
Here is the result:
The first two rectangles are what ItemsControl generates. The third one is what I have manually added right after the ItemsControl. As you can see, even though the code is exactly the same in both cases, the first two rectangles don't have names, but the third one has. Is there any reason why wouldn't it work with ItemsControl?
Edit:
Here is the code of the MyUserControl.xaml.cs:
public partial class MyUserControl : UserControl
{
public MyUserControl()
{
InitializeComponent();
SetValue(NamesProperty, new ObservableCollection<string>());
}
public static readonly DependencyProperty NamesProperty = DependencyProperty.Register(
"Names", typeof(ObservableCollection<string>), typeof(MyUserControl),
new PropertyMetadata(null, NamesPropertyChanged));
public ObservableCollection<string> Names
{
get { return (ObservableCollection<string>)GetValue(NamesProperty); }
set { SetValue(NamesProperty, value); }
}
private static void NamesPropertyChanged(DependencyObject obj, DependencyPropertyChangedEventArgs e)
{
var control = (MyUserControl)obj;
var oldCollection = e.OldValue as INotifyCollectionChanged;
var newCollection = e.NewValue as INotifyCollectionChanged;
if (oldCollection != null)
oldCollection.CollectionChanged -= control.NamesCollectionChanged;
if (newCollection != null)
newCollection.CollectionChanged += control.NamesCollectionChanged;
control.UpdateNames();
}
private void NamesCollectionChanged(object sender, NotifyCollectionChangedEventArgs e)
{
UpdateNames();
}
private void UpdateNames()
{
NamesPanel.Children.Clear();
if (Names == null)
return;
foreach(var name in Names)
{
var textBlock = new TextBlock();
textBlock.Text = name + ", ";
NamesPanel.Children.Add(textBlock);
}
}
}
MyUserControl.xaml:
<UserControl x:Class="TestItemsControl.MyUserControl"
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:TestItemsControl"
mc:Ignorable="d"
d:DesignHeight="300"
d:DesignWidth="300"
Name="ParentControl">
<StackPanel Name="NamesPanel" Orientation="Horizontal"/>
</UserControl>
Replace SetValue in the UserControl's constructor by SetCurrentValue. It may even make sense not to assign an initial value at all for the Names property.
public MyUserControl()
{
InitializeComponent();
SetCurrentValue(NamesProperty, new ObservableCollection<string>());
}
SetValue (as opposed to SetCurrentValue) sets a so-called local value to the Names property. When you assign a Binding as in the second case, this is also considered a local value with the same precedence as the one set in the constructor.
However, in the first case, the Binding is set in a DataTemplate, where it doesn't count as a local value. Since it has lower precedence, it does not replace the initial value.
More details here: Dependency Property Value Precedence
I must say that I really dislike the LongListSelector in WP8 and like the toolkit version so much better.
First it is not MVVM compatible so I found this code to make it so.
public class LongListSelector : Microsoft.Phone.Controls.LongListSelector
{
public LongListSelector()
{
SelectionChanged += LongListSelector_SelectionChanged;
}
void LongListSelector_SelectionChanged(object sender, SelectionChangedEventArgs e)
{
SelectedItem = base.SelectedItem;
}
public static readonly DependencyProperty SelectedItemProperty =
DependencyProperty.Register(
"SelectedItem",
typeof(object),
typeof(LongListSelector),
new PropertyMetadata(null, OnSelectedItemChanged)
);
private static void OnSelectedItemChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
var selector = (LongListSelector)d;
selector.SelectedItem = e.NewValue;
}
public new object SelectedItem
{
get { return GetValue(SelectedItemProperty); }
set { SetValue(SelectedItemProperty, value); }
}
}
then I made a view model using mvvm light
public class MainViewModel : ViewModelBase
{
public MainViewModel()
{
MyList = new ObservableCollection<Test>
{
new Test
{
Name = "test 1"
},
new Test
{
Name = "test 2"
}
};
ButtonCmd = new RelayCommand(() => Hit());
}
private void Hit()
{
SelectedItem = null;
}
public ObservableCollection<Test> MyList { get; set; }
/// <summary>
/// The <see cref="SelectedItem" /> property's name.
/// </summary>
public const string SelectedItemPropertyName = "SelectedItem";
private Test selectedItem = null;
/// <summary>
/// Sets and gets the SelectedItem property.
/// Changes to that property's value raise the PropertyChanged event.
/// </summary>
public Test SelectedItem
{
get
{
return selectedItem;
}
set
{
if (value != null)
{
MessageBox.Show(value.Name);
}
if (selectedItem == value)
{
return;
}
RaisePropertyChanging(() => SelectedItem);
selectedItem = value;
RaisePropertyChanged(() => SelectedItem);
}
}
public RelayCommand ButtonCmd
{
get;
private set;
}
then I made a model
public class Test : ObservableObject
{
public string Name { get; set; }
}
then I made the xaml
<phone:PhoneApplicationPage
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:phone="clr-namespace:Microsoft.Phone.Controls;assembly=Microsoft.Phone"
xmlns:shell="clr-namespace:Microsoft.Phone.Shell;assembly=Microsoft.Phone"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:ignore="http://www.ignore.com"
xmlns:local="clr-namespace:MvvmLight2" x:Class="MvvmLight2.MainPage"
mc:Ignorable="d ignore"
FontFamily="{StaticResource PhoneFontFamilyNormal}"
FontSize="{StaticResource PhoneFontSizeNormal}"
Foreground="{StaticResource PhoneForegroundBrush}"
SupportedOrientations="Portrait"
Orientation="Portrait"
shell:SystemTray.IsVisible="True"
DataContext="{Binding Main, Source={StaticResource Locator}}">
<!--LayoutRoot is the root grid where all page content is placed-->
<Grid x:Name="LayoutRoot"
Background="Transparent">
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
<RowDefinition Height="*" />
</Grid.RowDefinitions>
<local:LongListSelector ItemsSource="{Binding MyList}" SelectedItem="{Binding SelectedItem, Mode=TwoWay}">
<local:LongListSelector.Resources>
<DataTemplate x:Key="ItemTemplate">
<Grid>
<TextBlock Text="{Binding Name}" HorizontalAlignment="Left" VerticalAlignment="Top" FontSize="48"/>
</Grid>
</DataTemplate>
</local:LongListSelector.Resources>
<local:LongListSelector.ItemTemplate>
<StaticResource ResourceKey="ItemTemplate"/>
</local:LongListSelector.ItemTemplate>
</local:LongListSelector>
<Button Content="Unselect" HorizontalAlignment="Left" Margin="142,81,0,0" Grid.Row="1" VerticalAlignment="Top" Command="{Binding ButtonCmd, Mode=OneWay}"/>
</Grid>
</phone:PhoneApplicationPage>
when I click on the first item in the list, the message box shows up, if I hit it again nothing happens. I then hit my button which nulls out the selectedItem(which in the old toolkit version would be enough) and try again and nothing happens.
Only way I can every select the first row is by selecting the second row which is really bad if say the list only has 1 item at any given time.
Weird thing is that a simple collection of strings does not even require me to set the SelectItem to null as it always seems to deselect but when it comes to complex types it is a no go.
I've used the same code and ran into the very same problem. Reason for it is that your SelectedItem property overshadows the base.SelectedItem property. When setting it to a new value, not only set your SelectedItem property but the base one as well:
public new object SelectedItem
{
get { return GetValue(SelectedItemProperty); }
set
{
SetValue(SelectedItemProperty, value);
base.SelectedItem = value;
}
}
Then you have a MVVM capable code and can reset the SelectedItem in your ViewModel as well (by setting it to null).
you can easily achieve via in selectionchanged event
void LongListSelector_SelectionChanged(object sender, SelectionChangedEventArgs e)
{
if((sender as LongListSelector).SelectedItem == null){
return;
}
SelectedItem = base.SelectedItem;
(sender as LongListSelector).SelectedItem = null;
}
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>