WPF UserControl DependencyProperty and INotifyPropertyChanged on an ObservableCollection - c#

In an attempted MVVM implimentation, I have a UserControl implementing INotifyPropertyChanged. OnFilePathsChanged seems to be called after FilePaths has the new value, so it just copies it over again. If I don't implement INotifyPropertyChanged OnPropertyChanged the VIEWMODEL never gets updated. I have read that using INotifyPropertyChanged together with a DependencyProperty is redundant but I can't figure out the best way to do this. What am I missing. Is there a cleaner way to implement a UserControl with a bound ObservableCollection?
UserControl code
private void LoadFileMenuItem_Click(object sender, RoutedEventArgs e)
{
OpenFileDialog dialog = new OpenFileDialog();
if (dialog.ShowDialog() == true)
{
FilePaths.Add(dialog.FileName);
OnPropertyChanged("FilePaths");
}
}
private void InitFilePathsProperty()
{
this.PropertyChanged += new System.ComponentModel.PropertyChangedEventHandler(FilePaths_PropertyChanged);
}
void FilePaths_PropertyChanged(object sender, System.ComponentModel.PropertyChangedEventArgs e)
{
this.SetValue(FilePathsProperty, ImageFilePaths);
}
public static readonly DependencyProperty FilePathsProperty = DependencyProperty.Register("FilePaths", typeof(ObservableCollection<string>), typeof(FileDisplayControl),
new PropertyMetadata(OnImageFilePathsChanged));
private static void OnFilePathsChanged(DependencyObject obj, DependencyPropertyChangedEventArgs args)
{
if (args.NewValue != null)
{
var myFileDisplayControl = obj as FileDisplayControl;
if (myFileDisplayControl != null &&
myFileDisplayControl.FilePaths != null)
{
FilePaths = this.GetValue(args.Property) as ObservableCollection<string>;
}
}
}
public ObservableCollection<string> FilePaths
{
get { return this.GetValue(FilePathsProperty) as ObservableCollection<string>; }
set { this.SetValue(FilePathsProperty, value); }
}
In the VIEWMODEL
public ObservableCollection<string> ViewModelFilePaths
{
get { return _viewModelFilePaths; }
set { _viewModelFilePaths = value; }
}

It's a bit unclear as to what you're actually asking, but I think that this will help you whatever you're asking. If you have a view model that is data bound to a UserControl.DataContext, you can still data bind to any DependencyPropertys defined in that control using a RelativeSource Binding. Take this example from inside the UserControl:
Binding to the UserControl DependencyProperty:
<ListBox ItemsSource="{Binding FilePaths, RelativeSource={RelativeSource
AncestorType={x:Type YourPrefix:YourUserControl}}}" ... />
Binding to the data bound view model property:
<ListBox ItemsSource="{Binding ViewModelFilePaths}" ... />
You should implement the INotifyPropertyChanged interface in the view model, not the UserControl... at least, it is more customary to declare DependencyPropertys in a UserControl. However, if you have declared normal CLR properties that you want to data bind to in the UserControl, then you should implement the INotifyPropertyChanged interface there too.

Related

UWP Toolkit DataGrid - How to bind CollectionViewSource to ItemsSource of DataGrid?

I'm trying to bind a grouped collection of data items to a DataGrid. The details of the presented data are not relevant, in fact all the contents are set up with dummy data for now.
I followed the sample code found in Microsoft's Sample App and "How to: Group, sort and filter data in the DataGrid Control".
After launching the app the shown DataGrid is empty and the debug output from the binding code says:
Error: Converter failed to convert value of type 'Windows.UI.Xaml.Data.ICollectionView' to type 'IBindableIterable'; BindingExpression: Path='MyContents' DataItem='MyViewModel'; target element is 'Microsoft.Toolkit.Uwp.UI.Controls.DataGrid' (Name='null'); target property is 'ItemsSource' (type 'IBindableIterable').
This is the interesting part of my XAML:
<mstkcontrols:DataGrid ItemsSource="{Binding MyContents}">
<!-- Irrelevant stuff left out... -->
</mstkcontrols:DataGrid>
In my view model I have this code:
public ICollectionView MyContents { get; private set; }
public override void OnNavigatedTo(NavigationEventArgs e)
{
// Irrelevant stuff left out...
ObservableCollection<ObservableCollection<MyItemType>> groupedCollection = new ObservableCollection<ObservableCollection<MyItemType>>();
// It doesn't matter how this grouped collection is filled...
CollectionViewSource collectionViewSource = new CollectionViewSource();
collectionViewSource.IsSourceGrouped = true;
collectionViewSource.Source = groupedCollection;
MyContents = collectionViewSource.View;
}
Is there a conversion from ICollectionView to IBindableIterable? If so, how is it done?
I'm well aware that the examples do the binding in the code, not in the XAML. Does this really make a difference?
If this approach is wrong, how is the correct approach?
Edit:
I'm sorry, I forgot to mention that we use the "MVVM Light Toolkit" by GalaSoft. That's why the code to build the collection is in the view model, not the code behind. And it should stay there.
This has an impact on the kind of binding. To bind to a property of the view model, we use:
<mstkcontrols:DataGrid ItemsSource="{Binding MyContents}">
But to bind to a property of the code behind, is has to be:
<mstkcontrols:DataGrid ItemsSource="{x:Bind MyContents}">
In the meantime, thank you very much to all reading and making suggestions. I'm currently investigating how to connect view model and code behind.
Alright, it took me a 2-digit number of hours to find the root of this problem. There seems to be a disrupted way with Binding compared to x:Bind.
"{Binding} assumes, by default, that you're binding to the DataContext of your markup page." says the documentation "Data binding in depth". And the data context of my page is the view model.
"{x:Bind} does not use the DataContext as a default source—instead, it uses the page or user control itself." says the documentation "{x:Bind} markup extension". Well, and the compile-time generated code has no problems with the different data types.
The XAML is changed to (the Mode is important, because the default is OneTime):
<mstkcontrols:DataGrid ItemsSource="{x:Bind MyContents, Mode=OneWay}" Loaded="DataGrid_Loaded">
<!-- Irrelevant stuff left out... -->
</mstkcontrols:DataGrid>
The code behind needs a property that sends notification events. For this its class needs to inherit from INotifyPropertyChanged. You could use the methods Set() and OnPropertyChanged() shown in #NicoZhu's answer, but this cut-out shows more clearly what is important:
private ICollectionView _myContents;
public ICollectionView MyContents
{
get
{
return _myContents;
}
set
{
if (_myContents != value)
{
_myContents = value;
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(MyContents)));
}
}
}
public event PropertyChangedEventHandler PropertyChanged;
private void DataGrid_Loaded(object sender, RoutedEventArgs e)
{
if ((sender as DataGrid).DataContext is MyViewModel viewModel)
{
MyContents = viewModel.ContentsView();
}
}
The view model provides the contents view (as a collection of collections) through a method that is called from the code behind. This method is almost identical to the code I used before.
internal ICollectionView ContentsView()
{
ObservableCollection<ObservableCollection<MyItemType>> groupedCollection = new ObservableCollection<ObservableCollection<MyItemType>>();
// It doesn't matter how this grouped collection is filled...
CollectionViewSource collectionViewSource = new CollectionViewSource();
collectionViewSource.IsSourceGrouped = true;
collectionViewSource.Source = groupedCollection;
return collectionViewSource.View;
}
I follow this tutorial creating a simple sample to reproduce your issue, And binding CollectionViewSource works well. Please refer the following code. This is sample project.
Xaml
<controls:DataGrid
HorizontalAlignment="Stretch"
VerticalAlignment="Stretch"
AlternatingRowBackground="Transparent"
AlternatingRowForeground="Gray"
AreRowDetailsFrozen="False"
AreRowGroupHeadersFrozen="True"
AutoGenerateColumns="False"
CanUserReorderColumns="True"
CanUserResizeColumns="True"
CanUserSortColumns="False"
ColumnHeaderHeight="32"
FrozenColumnCount="0"
GridLinesVisibility="None"
HeadersVisibility="Column"
HorizontalScrollBarVisibility="Visible"
IsReadOnly="False"
ItemsSource="{x:Bind GroupView, Mode=TwoWay}"
Loaded="DataGrid_Loaded"
MaxColumnWidth="400"
RowDetailsVisibilityMode="Collapsed"
RowGroupHeaderPropertyNameAlternative="Range"
SelectionMode="Extended"
VerticalScrollBarVisibility="Visible"
>
<controls:DataGrid.RowGroupHeaderStyles>
<Style TargetType="controls:DataGridRowGroupHeader">
<Setter Property="Background" Value="LightGray" />
</Style>
</controls:DataGrid.RowGroupHeaderStyles>
<controls:DataGrid.Columns>
<controls:DataGridTextColumn
Binding="{Binding Name}"
Header="Rank"
Tag="Rank"
/>
<controls:DataGridComboBoxColumn
Binding="{Binding Complete}"
Header="Mountain"
Tag="Mountain"
/>
</controls:DataGrid.Columns>
</controls:DataGrid>
Code Behind
public sealed partial class MainPage : Page, INotifyPropertyChanged
{
public ObservableCollection<Item> MyClasses { get; set; } = new ObservableCollection<Item>();
private ICollectionView _groupView;
public ICollectionView GroupView
{
get
{
return _groupView;
}
set
{
Set(ref _groupView, value);
}
}
public MainPage()
{
this.InitializeComponent();
MyClasses.Add(new Item { Name = "Nico", Complete = false });
MyClasses.Add(new Item { Name = "LIU", Complete = true });
MyClasses.Add(new Item { Name = "He", Complete = true });
MyClasses.Add(new Item { Name = "Wei", Complete = false });
MyClasses.Add(new Item { Name = "Dong", Complete = true });
MyClasses.Add(new Item { Name = "Ming", Complete = false });
}
private void DataGrid_Loaded(object sender, RoutedEventArgs e)
{
var groups = from c in MyClasses
group c by c.Complete;
var cvs = new CollectionViewSource();
cvs.Source = groups;
cvs.IsSourceGrouped = true;
var datagrid = sender as DataGrid;
GroupView = cvs.View;
}
public event PropertyChangedEventHandler PropertyChanged;
private void Set<T>(ref T storage, T value, [CallerMemberName]string propertyName = null)
{
if (Equals(storage, value))
{
return;
}
storage = value;
OnPropertyChanged(propertyName);
}
private void OnPropertyChanged(string propertyName) => PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
I don't know how transitive WPF C# is to UWP, but this is how I do my observable collection data binding in WPF
In my window's .cs:
public partial class MainWindowView : Window, INotifyPropertyChanged
{
public MainWindowView()
{
InitializeComponent();
this.data.ItemsSource = etc;
}
public event PropertyChangedEventHandler PropertyChanged;
public ObservableCollection<Stuff_NThings> etc = new ObservableCollection<Stuff_NThings>();
private void Button_Click(object sender, RoutedEventArgs e)
{
Stuff_NThings t = new Stuff_NThings();
t.stuff = 45;
t.moreStuff = 44;
t.things = 33;
t.moreThings = 89;
etc.Add(t);
}
My class:
public class Stuff_NThings : INotifyPropertyChanged
{
private int _things;
private int _moreThings;
private int _stuff;
private int _moreStuff;
public int things
{
get
{
return _things;
}
set
{
_things = value;
NotifyPropertyChanged(nameof(things));
}
}
public int moreThings
{
get
{
return _moreThings;
}
set
{
_moreThings = value;
NotifyPropertyChanged(nameof(moreThings));
}
}
public int stuff
{
get
{
return _stuff;
}
set
{
_stuff = value;
NotifyPropertyChanged(nameof(stuff));
}
}
public int moreStuff
{
get
{
return _moreStuff;
}
set
{
_moreStuff = value;
NotifyPropertyChanged(nameof(moreStuff));
}
}
public event PropertyChangedEventHandler PropertyChanged;
private void NotifyPropertyChanged([CallerMemberName] string propertyName = "")
{
if (PropertyChanged != null)
{
PropertyChanged(this, new PropertyChangedEventArgs(propertyName));
}
}
}
By setting the dataGrid's item source in the mainWindow constructor, it will automatically create the headers in the dataGrid based on the class variable names. Whenever you add an instance of Stuff'NThings (via button, other, whatever, and etc) to the observable collection, the trigger is thrown and it updates the UI. Hope some of this actually applies!

WPF MVVM - Update Datagrid of the MainView from a child viewmodel

I think it may be quite simple but I have been googling a solution for a while without success, and I am not sure what is the correct approach. I saw that Prism could be a solution but I am looking for something simple.
I have a MainViewModel and a MainView which contains a Datagrid. The MainView contains also a ContentControl which display a ChildView (with DataContext as ChildViewModel).
I use DataTemplate to associate Views and ViewModels.
The DataGrid of the MainView is Binded to an ObservableCollection of the ChildViewModel.
I want this DataGrid to be updated every time this Collection is modified.
I have tried to use the INotifyPropertyChanged.
I have tried to use the OnCollectionChanged.
When I debug I can see that the Collection has changed and that event is fired but how to refresh the binding ? (dataGrid is not updated).
Here is the code:
DataTemplate
<DataTemplate DataType="{x:Type vm:ChildViewModel}">
<vi:ChildView />
</DataTemplate>
XAML MainView
<ContentControl Content="{Binding childViewModel}"/>
<DataGrid x:Name="DataGridChildren"
ItemsSource="{Binding childViewModel.Children,Mode=TwoWay}"
SelectedItem="{Binding childViewModel.SelectedItem}" EnableRowVirtualization="True" IsReadOnly="True" AutoGeneratingColumn="DataGrid_AutoGeneratingColumn">
MainViewModel
public ChildViewModel childViewModel { get; set; }
public MainViewModel()
{
childViewModel = new ChildViewModel();
}
ViewModelBase
public abstract class ViewModelBase : INotifyPropertyChanged
{
public event PropertyChangedEventHandler PropertyChanged;
protected virtual void OnPropertyChanged(string propertyName)
{
OnPropertyChanged(new PropertyChangedEventArgs(propertyName));
}
protected virtual void OnPropertyChanged(PropertyChangedEventArgs args)
{
var handler = PropertyChanged;
handler?.Invoke(this, args);
}
}
ChildViewModel
public class ChildViewModel: ViewModelBase
{
private ObservableCollection<Child> _Children;
public ObservableCollection<Child> Children
{
get { return _Children; }
set
{
_Children = value;
OnPropertyChanged("Children");
_Children.CollectionChanged +=handler;
}
}
private void handler(object sender, System.Collections.Specialized.NotifyCollectionChangedEventArgs e)
{
//What to do ?
}
public ChildViewModel()
{
Children = new ObservableCollection<Child>(ChildService.GetAllChild());
}`
Edit #lezhkin11
Yes I think that is the problem. But I have no idea how to fix it
Code behind MainView
void MainView_Loaded(object sender, RoutedEventArgs e)
{
mainViewModel = new MainViewModel();
DataContext = mainViewModel;
}
Code behind ChildView
void ChildView_Loaded(object sender, RoutedEventArgs e)
{
childViewModel = new ChildViewModel();
DataContext = childViewModel;
}
A button allow to do a new search:
private void BtnRefresf_Click(object sender, RoutedEventArgs e) // Search
{
childViewModel.Search();
}
Then a method in the childViewModel will use the service to give a new value to the Observable collection (when I debug, I can reach the OnpropertyChanged of the collection)
public void Search()
{
ObservableCollection<Child> collec = new ObservableCollection<Child>(ChildService.GetAllChild());
Children = collec;
}
Your DataGrid is bound to ChildViewModel inside MainViewModel (instance 1). While ChildView has it's own ChildViewModel (instance 2).
You have to make sure that both controls reference to the same instance.
1) Remove completely public ChildViewModel childViewModel { get; set; } from MainViewModel
2) Give a name to the ChildView.
<vi:ChildView x:Name="MyChild" />
3) Bind DataGrid's properties to correct view-model
<DataGrid ItemsSource="{Binding DataContext.Children, ElementName=MyChild, Mode=TwoWay}" />
Also, I would recommend initializing DataContext directly in constructor.
Loaded event is too late - as result you always have lot's of XAML binding exceptions that affect performance (even can lead to unexpected behavior)

WPF TreeView CheckBox Binding - How to populate ViewModel with checked boxes

I'm slightly confused about how to set up a CheckBox with a binding that ensures that my ViewModel is populated with all the checked fields. I have provided some of the code and a description at the bottom.
My Xaml file let's call it TreeView.xaml:
<TreeView x:Name="availableColumnsTreeView"
ItemsSource="{Binding Path=TreeFieldData, Mode=OneWay, Converter={StaticResource SortingConverter}, ConverterParameter='DisplayName.Text'}">
<TreeView.ItemTemplate>
<HierarchicalDataTemplate x:Uid="HierarchicalDataTemplate_1" ItemsSource="{Binding Path=Children, Mode=OneWay, Converter={StaticResource SortingConverter}, ConverterParameter='DisplayName.Text'}">
<CheckBox VerticalAlignment="Center" IsChecked="{Binding IsSelected, Mode=TwoWay}">
<TextBlock x:Uid="TextBlock_1" Text="{Binding DisplayName.Text, Mode=OneWay}" />
</CheckBox>
</HierarchicalDataTemplate>
</TreeView.ItemTemplate>
</TreeView>
The "code behind" TreeView.xaml.cs
public partial class MultipleColumnsSelectorView : UserControl
{
public MultipleColumnsSelectorView()
{
InitializeComponent();
}
private MultipleColumnsSelectorVM Model
{
get { return DataContext as MultipleColumnsSelectorVM; }
}
}
The ViewModel (tried to include only the relevant stuff) MultipleColumnsSelectorVM:
public partial class MultipleColumnsSelectorVM : ViewModel, IMultipleColumnsSelectorVM
{
public ReadOnlyCollection<TreeFieldData> TreeFieldData
{
get { return GetValue(Properties.TreeFieldData); }
set { SetValue(Properties.TreeFieldData, value); }
}
public List<TreeFieldData> SelectedFields
{
get { return GetValue(Properties.SelectedFields); }
set { SetValue(Properties.SelectedFields, value); }
}
private void AddFields()
{
//Logic which loops over SelectedFields and when done calls a delegate which passes
//the result to another class. This works, implementation hidden
}
The model TreeFieldData:
public class TreeFieldData : INotifyPropertyChanged
{
public event PropertyChangedEventHandler PropertyChanged;
public IEnumerable<TreeFieldData> Children { get; private set; }
private bool _isSelected;
public bool IsSelected
{
get { return _isSelected; }
set
{
_isSelected = value;
if (PropertyChanged != null)
PropertyChanged.Invoke(this, new PropertyChangedEventArgs("IsSelected"));
}
}
}
The Problem:
The behaviour that I want is when the user checks a checkbox, it should set the IsSelected property of TreeField (it does that right now) but then I want to go back to the ViewModel and make sure that this specific TreeField is added to SelectedFields. I don't really understand what the PropertyChangedEvent.Invoke does and who will receive that event? How can I make sure that SelectedFields gets populated so when AddFields() is invoked it has all the TreeField data instances which were checked?
You could iterate through the TreeFieldData objects in the TreeFieldData collection and hook up an event handler to their PropertyChanged event and then add/remove the selected/unselected items from the SelectedFields collection, e.g.:
public MultipleColumnsSelectorVM()
{
Initialize();
//do this after you have populated the TreeFieldData collection
foreach (TreeFieldData data in TreeFieldData)
{
data.PropertyChanged += OnPropertyChanged;
}
}
private void OnPropertyChanged(object sender, System.ComponentModel.PropertyChangedEventArgs e)
{
if (e.PropertyName == "IsSelected")
{
TreeFieldData data = sender as TreeFieldData;
if (data.IsSelected && !SelectedFields.Contains(data))
SelectedFields.Add(data);
else if (!data.IsSelected && SelectedFields.Contains(data))
SelectedFields.Remove(data);
}
}
The subscriber of the PropertyChanged event is the view, so that if you change IsSelected programmatically the view knows it needs to update.
To insert the selected TreeField into your list you would add this code to your setter.
Also, you could define the following function which makes the notification much easier if you have many properties:
private void NotifyPropertyChange([CallerMemberName] string propertyName = null)
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
The CallerMemberName attribute instructs the compiler to automatically insert the name of the property calling the method. The ? after PropertyChanged is a shorthand to your comparison to not null.
The setter of IsSelected can then be changed to
set
{
_isSelected = value;
if (value) { viewModel.SelectedFields.Add(this); }
else { viewModel.SelectedFields.Remove(this); }
NotifyPropertyChange();
}
Of course you would need to provide the TreeFieldData with the ViewModel instance, e.g. in the constructor.
I don't know if SelectedFields is bounded/shown in your view. If yes and you want the changes made to the list to be shown, you should change List to ObservableCollection.

Change binding when focus changes

I have an application that has several treeviews and one propertygrid (from the extended WPF toolkit). The goal is to display the properties of the selected item. I'm fairly new to WPF so I started with one treeview and bind the propertygrids selected object like this
<xctk:PropertyGrid x:Name="xctkPropertyGrid"
Grid.Column="2"
ShowSearchBox="False"
ShowSortOptions="False"
SelectedObject="{Binding ElementName=actionsTreeView, Path=SelectedItem, Mode=OneWay}">
</xctk:PropertyGrid>
This seems to work fine. But it off course binds to actionsTreeView all the time. What I would really need is an update of that propertygrid when the focus changes to another selecteditem in another treeview. I have achieved my goal using the SelectedItemChanged of each treeview and set the propertygrids selectedobject like so. Is this somehow possible using databinding and triggers. My solution adds some code behind and tight coupling and that doesn't feel very MVVM.
kind regards,
Jef
Ok, here's how I ended up solving my problem:
Each treeview is bound to a viemodel property on the main viewmodel. I also created a SelectedItem property on the main viewmodel like this to which the propertygrid's SelectedObject is bound:
private object selectedItem;
public object SelectedItem
{
get { return selectedItem; }
set
{
selectedItem = value;
OnPropertyChanged("SelectedItem");
}
}
I then attach a behavior to each treeview that updates this SelectedItem:
public class UpdateSelectedItemBehavior : Behavior<TreeView>
{
protected override void OnAttached()
{
base.OnAttached();
this.AssociatedObject.GotFocus += AssociatedObject_GotFocus;
this.AssociatedObject.SelectedItemChanged += AssociatedObject_SelectedItemChanged;
}
void AssociatedObject_SelectedItemChanged(object sender, RoutedPropertyChangedEventArgs<object> e)
{
ViewModels.MainViewModel mainViewModel = AssociatedObject.DataContext as ViewModels.MainViewModel;
if (mainViewModel != null)
{
mainViewModel.SelectedItem = AssociatedObject.SelectedItem;
}
}
void AssociatedObject_GotFocus(object sender, RoutedEventArgs e)
{
ViewModels.MainViewModel mainViewModel = AssociatedObject.DataContext as ViewModels.MainViewModel;
if (mainViewModel != null)
{
mainViewModel.SelectedItem = AssociatedObject.SelectedItem;
}
}
}

Proper way to use CollectionViewSource in ViewModel

I used Drag and Drop to bind Data Source object (a DB model) to DataGrid (basically following this example in Entity Framework Databinding with WPF.
Everything works fine with this implementation.
XAML
<Window.Resources>
<CollectionViewSource x:Key="categoryViewSource"
d:DesignSource="{d:DesignInstance {x:Type local:Category}, CreateList=True}"/>
</Window.Resources>
<Grid DataContext="{StaticResource categoryViewSource}">
..
Code Behind
private void Window_Loaded(object sender, RoutedEventArgs e)
{
System.Windows.Data.CollectionViewSource categoryViewSource =
((System.Windows.Data.CollectionViewSource)(this.FindResource("categoryViewSource")));
_context.Categories.Load();
categoryViewSource.Source = _context.Categories.Local;
}
ViewModel
public MainWindow()
{
InitializeComponent();
this.DataContext = new MyViewModel();
}
However, when I try to use the same code from within ViewModel, it doesn‘t work (FindResource is not available), besides, I don’t think this is the right approach (i.e. to use x:Key in MVVM).
I would really appreciate any help to point me what is the right way to implement CollectionViewSource and DataBinding with DataGrid.
You have two options to use CollectionViewSource properly with MVVM -
Expose an ObservableCollection of items (Categories in your case) through your ViewModel and create CollectionViewSource in XAML like this -
<CollectionViewSource Source="{Binding Path=Categories}">
<CollectionViewSource.SortDescriptions>
<scm:SortDescription PropertyName="CategoryName" />
</CollectionViewSource.SortDescriptions>
</CollectionViewSource>
scm: xmlns:scm="clr-namespace:System.ComponentModel;assembly=Wind‌​owsBase"
see this - Filtering collections from XAML using CollectionViewSource
Create and Expose an ICollectionView directly from your ViewModel
see this - How to Navigate, Group, Sort and Filter Data in WPF
Following example shows how to create a collection view and
bind it to a ListBox
View XAML:
<Window
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:scm="clr-namespace:System.ComponentModel;assembly=WindowsBase"
x:Class="CustomerView">
<ListBox ItemsSource={Binding Customers} />
</Window>
View Codebehind:
public class CustomerView : Window
{
public CustomerView()
{
DataContext = new CustomerViewModel();
}
}
ViewModel:
public class CustomerViewModel
{
private readonly ICollectionView customerView;
public ICollectionView Customers
{
get { return customerView; }
}
public CustomerViewModel()
{
IList<Customer> customers = GetCustomers();
customerView = CollectionViewSource.GetDefaultView( customers );
}
}
Update:
Q. If there is no property to sort on? e.g. if there is an ObservableCollection of string or int?
A. In that case you can Simply use . as the property name:
<scm:SortDescription PropertyName="." />
I found that it is handy to have a CollectionViewSource in my ViewModel and bind the ListBox (in my case) to the CollectionViewSource.View while setting the CollectionViewSource.Source to be the list I want to use.
Like so:
ViewModel:
public DesignTimeVM() //I'm using this as a Design Time VM
{
Items = new List<Foo>();
Items.Add(new Foo() { FooProp= "1", FooPrep= 20.0 });
Items.Add(new Foo() { FooProp= "2", FooPrep= 30.0 });
FooViewSource = new CollectionViewSource();
FooViewSource.Source = Items;
SelectedFoo = Items.First();
//More code as needed
}
XAML:
<ListBox ItemsSource="{Binding FooViewSource.View}" SelectedItem="{Binding SelectedFoo}"/>
This means I can do neat stuff in the VM as needed (from https://blogs.msdn.microsoft.com/matt/2008/08/28/collectionview-deferrefresh-my-new-best-friend/ ):
using (FooViewSource.DeferRefresh())
{
//Remove an old Item
//add New Item
//sort list anew, etc.
}
I suppose this is possible when using the ICollectionView object also, but the demo code in the blog link seems to be some codebehind stuff, refering the listbox directly, which I'm trying to avoid.
BTW before you ask, here's how you use a Design Time VM: WPF Design Time View Model
Just for reference, another way is to use an attached property on the CollectionViewSource which then pipes the functions to the ViewModel (Implementing an Interface).
This is a very basic Demonstration just for filtering, it would need some work for e.g. a second Collection on the VM but i think it's enough to show the general technique.
If this is better or worse than the other methods is up for discussion, i just wanted to point out, that there's another way of doing this
Definition of attached Property:
public static class CollectionViewSourceFilter
{
public static IFilterCollectionViewSource GetFilterObject(CollectionViewSource obj)
{
return (IFilterCollectionViewSource)obj.GetValue(FilterObjectProperty);
}
public static void SetFilterObject(CollectionViewSource obj, IFilterCollectionViewSource value)
{
obj.SetValue(FilterObjectProperty, value);
}
public static void FilterObjectChanged(object sender, DependencyPropertyChangedEventArgs e)
{
if (e.OldValue is IFilterCollectionViewSource oldFilterObject
&& sender is CollectionViewSource oldCvs)
{
oldCvs.Filter -= oldFilterObject.Filter;
oldFilterObject.FilterRefresh -= (s, e2) => oldCvs.View.Refresh();
}
if (e.NewValue is IFilterCollectionViewSource filterObject
&& sender is CollectionViewSource cvs)
{
cvs.Filter += filterObject.Filter;
filterObject.FilterRefresh += (s,e2) => cvs.View.Refresh();
}
}
public static readonly DependencyProperty FilterObjectProperty = DependencyProperty.RegisterAttached(
"FilterObject",
typeof(Interfaces.IFilterCollectionViewSource),
typeof(CollectionViewSourceFilter),
new PropertyMetadata(null,FilterObjectChanged)
);
}
Interface:
public interface IFilterCollectionViewSource
{
void Filter(object sender, FilterEventArgs e);
event EventHandler FilterRefresh;
}
usage in xaml:
<CollectionViewSource
x:Key="yourKey"
Source="{Binding YourCollection}"
classes:CollectionViewSourceFilter.FilterObject="{Binding}" />
and usage in the ViewModel:
class YourViewModel : IFilterCollectionViewSource
{
public event EventHandler FilterRefresh;
private string _SearchTerm = string.Empty;
public string SearchTerm
{
get { return _SearchTerm; }
set {
SetProperty(ref _SearchTerm, value);
FilterRefresh?.Invoke(this, null);
}
}
private ObservableCollection<YourItemType> _YourCollection = new ObservableCollection<YourItemType>();
public ObservableCollection<YourItemType> YourCollection
{
get { return _YourCollection; }
set { SetProperty(ref _YourCollection, value); }
}
public void Filter(object sender, FilterEventArgs e)
{
e.Accepted = (e.Item as YourItemType)?.YourProperty?.ToLower().Contains(SearchTerm.ToLower()) ?? true;
}
}

Categories