How do objects NOT implementing INPC notify changes? - c#

Simple question for those who know the answer:
I have a basic Person class defined as follows:
public class Person
{
public Person(string name, string surname)
{
this.Name = name;
this.Surname = surname;
}
public string Name { get; set; }
public string Surname { get; set; }
}
a very simple ViewModel
public partial class SimpleViewModel : Screen
{
public SimpleViewModel()
{
this.Persons = new ObservableCollection<Person>(this.GetPersons());
}
private ObservableCollection<Person> persons;
public ObservableCollection<Person> Persons
{
get { return this.persons; }
set
{
if (this.persons == value) return;
this.persons = value;
this.NotifyOfPropertyChange(() => this.Persons);
}
}
private Person selectedPerson;
public Person SelectedPerson
{
get { return this.selectedPerson; }
set
{
if (this.selectedPerson == value) return;
this.selectedPerson = value;
this.NotifyOfPropertyChange(() => this.SelectedPerson);
}
}
IEnumerable<Person> GetPersons()
{
return
new Person[]
{
new Person("Casey", "Stoner"),
new Person("Giacomo", "Agostini"),
new Person("Troy", "Bayliss"),
new Person("Valentino", "Rossi"),
new Person("Mick", "Doohan"),
new Person("Kevin", "Schwantz")
};
}
}
and a very simple View
<Window x:Class="Test.SimpleView"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Title="SimpleView"
Width="300"
Height="300">
<Grid>
<Grid.RowDefinitions>
<RowDefinition />
<RowDefinition Height="8"/>
<RowDefinition Height="Auto"/>
</Grid.RowDefinitions>
<Grid.ColumnDefinitions>
<ColumnDefinition/>
</Grid.ColumnDefinitions>
<ListView x:Name="lsv"
ItemsSource="{Binding Persons}"
SelectedItem="{Binding SelectedPerson}" Grid.RowSpan="3">
<ListView.View>
<GridView>
<GridViewColumn DisplayMemberBinding="{Binding FirstName}" />
<GridViewColumn DisplayMemberBinding="{Binding LastName}" />
</GridView>
</ListView.View>
</ListView>
<StackPanel Grid.Row="2"
Grid.Column="1"
VerticalAlignment="Center">
<TextBox Text="{Binding SelectedPerson.FirstName, UpdateSourceTrigger=PropertyChanged}" />
<TextBox Text="{Binding SelectedPerson.LastName, UpdateSourceTrigger=PropertyChanged}" />
</StackPanel>
</Grid>
</Window>
if I edit FirstName or LastName in the textbox, the listview updates.
How is that possibile if Person doesn't implement INotifyPropertyChanged?
Thanks
P.S. ViewModel inherits Screen from Caliburn.Micro

This is using the PropertyDescriptor to propagate the change notifications as described here.
I wouldn't rely on this sort of binding.
It is slower and heavier weight than implementing INPC (best
practices suggestion for POCO objects).
It only works for changes
initiated through Binding syntax. If you were to programmatically
change the value of Name, the list would not respond.

I am not sure but I think........
Your Persons property implements INotifyPropertyChanged interface and so, Indirectly all the objects or properties in your Person class also implements INotifyPropertyChanged.
I might be wrong in this case. ObservableCollection also implements INotifyPropertyChanged internally. So, there is no requirement to implement INotifyPropertyChanged in Persons Property.

Related

WPF ListView SelectedItem Not Updating With Expander in DataTemplate

I'm creating a WPF application using C# and MVVM, and I'm having some trouble with the SelectedItem of a ListView.
In the contrived example, the scenario: I have a PersonViewModel containing a List of Person objects. Each person object has its own List of Address objects. My goal is to load all of the Person objects into a ListView. Additionally, the ListView DataTemplate should include Expanders that show the available addresses for each person.
The problem: This all actually works decently enough. The only problem is that if a user selects the expander for a person object, it doesn't actually change the SelectedItem property of the ListView (which is bound to a SelectedPerson object in the view model).
I'm relatively confident that the code works otherwise because if I click on the ListView row (outside the boundaries of the contained expander), the SelectedPerson property is updated. Is there anyway to somehow bind when the expander is clicked to the SelectedPerson property? (I'm also open to other UI ideas that will get my information across in a cleaner, more easily implemented manner).
Person.cs
using System.Collections.Generic;
namespace ListViewExpanderTest
{
public class Person
{
public Person()
{
Addresses = new List<Address>();
}
public string Name { get; set; }
public List<Address> Addresses { get; set; }
}
}
Address.cs
namespace ListViewExpanderTest
{
public class Address
{
public int HouseNumber { get; set; }
public string StreetName { get; set; }
public string City { get; set; }
}
}
PersonViewModel.cs
using System.Collections.Generic;
using System.ComponentModel;
using System.Runtime.CompilerServices;
namespace ListViewExpanderTest
{
public class PersonViewModel : INotifyPropertyChanged
{
private Person _selectedPerson;
private Address _selectedAddress;
public List<Person> People { get; set; }
public Person SelectedPerson
{
get
{
return _selectedPerson;
}
set
{
_selectedPerson = value;
OnPropertyChanged();
}
}
public Address SelectedAddress
{
get
{
return _selectedAddress;
}
set
{
_selectedAddress = value;
OnPropertyChanged();
}
}
public PersonViewModel()
{
People = new List<Person>
{
new Person
{
Name = "Person 1",
Addresses = new List<Address>
{
new Address {HouseNumber = 1, StreetName = "Fake St", City = "Fake City" },
new Address {HouseNumber = 2, StreetName = "Super Fake St", City = "Super Fake City" }
}
},
new Person
{
Name = "Person 2",
Addresses = new List<Address>
{
new Address {HouseNumber = 10, StreetName = "Fake St", City = "Fake City" },
new Address {HouseNumber = 20, StreetName = "Super Fake St", City = "Super Fake City" }
}
}
};
}
public event PropertyChangedEventHandler PropertyChanged;
protected void OnPropertyChanged([CallerMemberName] string propertyName = "")
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
}
}
MainWindow.xaml
<Window x:Class="ListViewExpanderTest.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:ListViewExpanderTest"
mc:Ignorable="d"
Title="MainWindow" Height="350" Width="525">
<Window.DataContext>
<local:PersonViewModel />
</Window.DataContext>
<Grid>
<ListView ItemsSource="{Binding People, Mode=TwoWay}"
SelectedItem="{Binding SelectedPerson}">
<ListView.ItemTemplate>
<DataTemplate>
<Expander >
<Expander.Header>
<StackPanel Orientation="Horizontal">
<TextBlock Text="{Binding Name}" />
</StackPanel>
</Expander.Header>
<Expander.Content>
<ListView ItemsSource="{Binding Addresses, Mode=TwoWay}"
SelectedItem="{Binding SelectedAddress}">
<ListView.View>
<GridView>
<GridViewColumn Width="50" Header="Number" DisplayMemberBinding="{Binding HouseNumber}" />
<GridViewColumn Width="100" Header="Street" DisplayMemberBinding="{Binding StreetName}" />
<GridViewColumn Width="100" Header="City" DisplayMemberBinding="{Binding City}" />
</GridView>
</ListView.View>
</ListView>
</Expander.Content>
</Expander>
</DataTemplate>
</ListView.ItemTemplate>
</ListView>
</Grid>
</Window>
As your view model contains a single SelectedPerson property it doesn't make sense to me to include an expander in the datatemplate. If you can change your UI, the below example seems to make more sense to me. You can obviously place the address 'panel' anywhere you like.
Also, have you tried selecting an address from the sub listview? Does the binding work?
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition></ColumnDefinition>
<ColumnDefinition></ColumnDefinition>
</Grid.ColumnDefinitions>
<ListView ItemsSource="{Binding People, Mode=TwoWay}"
SelectedItem="{Binding SelectedPerson}"
DisplayMemberPath="Name">
</ListView>
<ListView Grid.Column="1"
ItemsSource="{Binding SelectedPerson.Addresses, Mode=TwoWay}"
SelectedItem="{Binding SelectedAddress}">
<ListView.View>
<GridView>
<GridViewColumn Width="50" Header="Number" DisplayMemberBinding="{Binding HouseNumber}" />
<GridViewColumn Width="100" Header="Street" DisplayMemberBinding="{Binding StreetName}" />
<GridViewColumn Width="100" Header="City" DisplayMemberBinding="{Binding City}" />
</GridView>
</ListView.View>
</ListView>
</Grid>

Dynamically filtering a CollectionViewSource bound to a SelectedItem via MVVM

I've been digging back into some projects in WPF, and come across a hurdle that I haven't been able to find a directly related solution for.
Essentially I want to filter a child property of a SelectedItem dynamically (via text entered in the filter box, something along the lines of .Contains(filter)). The UI displays correctly in the sample project, but after attempting to implement solutions from every hit possible on SO or otherwise, I've come up blank, or making serious compromises to the MVVM pattern.
ParentItem:
public class ParentItem
{
public string Name { get; set; }
public List<string> ChildItems { get; set; }
public DateTime CreatedOn { get; set; }
public bool IsActive { get; set; }
public ParentItemStatus Status { get; set; }
}
public enum ParentItemStatus
{
Status_One,
Status_Two
}
ViewModel:
public class MainWindowViewModel : ViewModelBase
{
public ObservableCollection<ParentItem> ParentItems { get; set; }
public MainWindowViewModel()
{
ParentItems = new ObservableCollection<ParentItem>();
LoadDummyParentItems();
}
private ICommand _filterChildrenCommand;
public ICommand FilterChildrenCommand => _filterChildrenCommand ?? (_filterChildrenCommand = new RelayCommand(param => FilterChildren((string)param), param => CanFilterChildren((string)param)));
private bool CanFilterChildren(string filter)
{
//TODO: Check for selected item in real life.
return filter.Length > 0;
}
private void FilterChildren(string filter)
{
//TODO: Filter?
}
private void LoadDummyParentItems()
{
for (var i = 0; i < 20; i++)
{
ParentItems.Add(new ParentItem()
{
Name = $"Parent Item {i}",
CreatedOn = DateTime.Now.AddHours(i),
IsActive = i % 2 == 0 ? true : false,
Status = i % 2 == 0 ? ParentItemStatus.Status_Two : ParentItemStatus.Status_One,
ChildItems = new List<string>() { $"Child one_{i}", $"Child two_{i}", $"Child three_{i}", $"Child four_{i}" }
});
}
}
}
MainWindow:
<Window x:Class="FilteringDemo.Views.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:FilteringDemo.Views"
mc:Ignorable="d"
Title="MainWindow" Height="450" Width="800">
<Window.Resources>
<CollectionViewSource x:Key="ChildItemsViewSource" Source="{Binding ElementName=ItemList, Path=SelectedItem.ChildItems}" />
</Window.Resources>
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width=".25*"/>
<ColumnDefinition Width=".75*"/>
</Grid.ColumnDefinitions>
<ListView x:Name="ItemList" Grid.Column="0" Margin="2" ItemsSource="{Binding ParentItems}" SelectionMode="Single">
<ListView.ItemTemplate>
<DataTemplate>
<TextBlock Text="{Binding Name}"/>
</DataTemplate>
</ListView.ItemTemplate>
</ListView>
<Grid Grid.Column="1">
<Grid.RowDefinitions>
<RowDefinition Height="Auto"/>
<RowDefinition Height="1*"/>
<RowDefinition Height="Auto"/>
</Grid.RowDefinitions>
<Grid Grid.Row="0">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="1*"/>
<ColumnDefinition Width="1*"/>
<ColumnDefinition Width="1*"/>
<ColumnDefinition Width="1*"/>
</Grid.ColumnDefinitions>
<TextBlock Grid.Column="0" Text="{Binding ElementName=ItemList, Path=SelectedItem.Name}" Margin="2"/>
<TextBlock Grid.Column="1" Text="{Binding ElementName=ItemList, Path=SelectedItem.CreatedOn}" Margin="2"/>
<TextBlock Grid.Column="2" Text="{Binding ElementName=ItemList, Path=SelectedItem.IsActive}" Margin="2"/>
<TextBlock Grid.Column="3" Text="{Binding ElementName=ItemList, Path=SelectedItem.Status}" Margin="2"/>
</Grid>
<ListView Grid.Row="1" Margin="2" ItemsSource="{Binding Source={StaticResource ChildItemsViewSource}}" />
<Grid Grid.Row="2">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto"/>
<ColumnDefinition Width="1*"/>
<ColumnDefinition Width="Auto"/>
</Grid.ColumnDefinitions>
<TextBlock Grid.Column="0" Text="Contains:" Margin="2" VerticalAlignment="Center"/>
<TextBox x:Name="ChildFilterInput" Grid.Column="1" Margin="2" />
<Button Grid.Column="2" Content="Filter" Width="100" Margin="2" Command="{Binding FilterChildrenCommand}" CommandParameter="{Binding ElementName=ChildFilterInput, Path=Text}"/>
</Grid>
</Grid>
</Grid>
</Window>
I've tried various implementations of adding a Filter event handler on the CollectionViewSource but have been unable to make them dynamic. It also seems like most examples/tutorials only deal directly with the parent item or static filters.
In a non-MVVM mindset, I was thinking to have an interaction trigger drive the selected item back into the ViewModel, and then create a filtered ICollectionView which the ChildItems ListView would bind to, but it seems like I can't be the only person trying this, and that there must be an easier MVVM binding friendly way.
The following example shows a simple solution to implement live filtering on a collection:
Person.cs
class Person
{
public Person(string firstName, string lastName)
{
this.FirstName = firstName;
this.LastName = lastName;
}
public string FirstName { get; set; }
public string LastName { get; set; }
}
ViewModel.cs
class ViewModel
{
public ViewModel()
{
this.Persons = new ObservableCollection<Person>()
{
new Person("Derek", "Zoolander"),
new Person("Tony", "Montana"),
new Person("John", "Wick"),
new Person("The", "Dude"),
new Person("James", "Bond"),
new Person("Walter", "White")
};
}
private void FilterData(string filterPredicate)
{
// Execute live filter
CollectionViewSource.GetDefaultView(this.Persons).Filter =
item => (item as Person).FirstName.StartsWith(filterPredicate, StringComparison.OrdinalIgnoreCase);
}
private string searchPredicate;
public string SearchPredicate
{
get => this.searchFilter;
set
{
this.searchPredicate = value;
FilterData(value);
}
}
public ObservableCollection<Person> Persons { get; set; }
}
MainWindow.xaml
<Window>
<Window.DataContext>
<ViewModel />
</Window.DataContext>
<StackPanel>
<TextBox Text="{Binding SearchPredicate, UpdateSourceTrigger=PropertyChanged"} />
<ListView ItemsSource="{Binding Persons}">
<ListView.View>
<GridView>
<GridView.Columns>
<GridViewColumn Header="Firstname" DisplayMemberBinding="{Binding FirstName}" />
<GridViewColumn Header="Lastname" DisplayMemberBinding="{Binding LastName}" />
</GridView.Columns>
</GridView>
</ListView.View>
</ListView>
</StackPanel>
</Window>
Update
It seems like you are having problems to filter the child items. The following example is more specific to your scenario:
DataItem.cs
class DataItem
{
public DataItem(string Name)
{
this.Name = name;
}
public string Name { get; set; }
public ObservableCollection<DataItem> ChildItems { get; set; }
}
ViewModel.cs
class ViewModel
{
public ViewModel()
{
this.ParentItems = new ObservableCollection<DataItem>()
{
new DataItem("Ben Stiller") { ChildItems = new ObservableCollection<DataItem>() { new DataItem("Zoolander"), new DataItem("Tropical Thunder") }},
new DataItem("Al Pacino") { ChildItems = new ObservableCollection<DataItem>() { new DataItem("Scarface"), new DataItem("The Irishman") }},
new DataItem("Keanu Reeves") { ChildItems = new ObservableCollection<DataItem>() { new DataItem("John Wick"), new DataItem("Matrix") }},
new DataItem("Bryan Cranston") { ChildItems = new ObservableCollection<DataItem>() { new DataItem("Breaking Bad"), new DataItem("Malcolm in the Middle") }}
};
}
private void FilterData(string filterPredicate)
{
// Execute live filter
CollectionViewSource.GetDefaultView(this.SelectedParentItem.ChildItems).Filter =
item => (item as DataItem).Name.StartsWith(filterPredicate, StringComparison.OrdinalIgnoreCase);
}
private string searchPredicate;
public string SearchPredicate
{
get => this.searchFilter;
set
{
this.searchPredicate = value;
FilterData(value);
}
}
public ObservableCollection<DataItem> ParentItems { get; set; }
public DataItem SelectedParentItem { get; set; }
}
MainWindow.xaml
<Window>
<Window.DataContext>
<ViewModel />
</Window.DataContext>
<StackPanel>
<ListView ItemsSource="{Binding ParentItems}"
SelectedItem="{Binding SelectedParentItem}">
<ListView.ItemTemplate>
<DataTemplate>
<TextBlock Text="{Binding Name}"/>
</DataTemplate>
</ListView.ItemTemplate>
</ListView>
<TextBox Text="{Binding SearchPredicate, UpdateSourceTrigger=PropertyChanged}" />
<ListView ItemsSource="{Binding SelectedParentItem.ChildItems}" />
</StackPanel>
</Window>
Using the examples from #BionicCode- I added a SelectedParentItem INPC property to the ViewModel, performed the filtering on that via CollectionViewSource.Filter, and bound the ChildItems ListView to SelectedParentItem.ChildItems.
I did not bind the text box property changed to a backing field in the VM as per #BionicCode's example, as the "real" ChildItems might be in the mid 10,000's and I didn't want it to filter on each keystroke. So this answer implements the filter button and text box command, and the CanFilterChildren is doing a proper null check.
MainWindowViewModel.cs:
public class MainWindowViewModel : ViewModelBase
{
public ObservableCollection<ParentItem> ParentItems { get; set; }
private ParentItem _selectedParentItem;
public ParentItem SelectedParentItem
{
get { return _selectedParentItem; }
set { SetProperty(ref _selectedParentItem, value); }
}
public MainWindowViewModel()
{
ParentItems = new ObservableCollection<ParentItem>();
LoadDummyParentItems();
}
private ICommand _filterChildrenCommand;
public ICommand FilterChildrenCommand => _filterChildrenCommand ?? (_filterChildrenCommand = new RelayCommand(param => FilterChildren((string)param), param => CanFilterChildren((string)param)));
private bool CanFilterChildren(string filter) => SelectedParentItem != null && filter.Length > 0;
private void FilterChildren(string filter)
{
CollectionViewSource.GetDefaultView(SelectedParentItem.ChildItems).Filter = item => (item as string).Contains(filter);
}
private void LoadDummyParentItems()
{
for (var i = 0; i < 20; i++)
{
ParentItems.Add(new ParentItem()
{
Name = $"Parent Item {i}",
CreatedOn = DateTime.Now.AddHours(i),
IsActive = i % 2 == 0 ? true : false,
Status = i % 2 == 0 ? ParentItemStatus.Status_Two : ParentItemStatus.Status_One,
ChildItems = new List<string>() { $"Child one_{i}", $"Child two_{i}", $"Child three_{i}", $"Child four_{i}" }
});
}
}
}
MainWindow.xaml.cs:
public partial class MainWindow : Window
{
private readonly MainWindowViewModel _viewModel;
public MainWindow()
{
InitializeComponent();
_viewModel = new MainWindowViewModel();
this.DataContext = _viewModel;
}
}
MainWindow.xaml:
<Window x:Class="FilteringDemo.Views.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:FilteringDemo.Views"
mc:Ignorable="d"
Title="MainWindow" Height="450" Width="800">
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width=".25*"/>
<ColumnDefinition Width=".75*"/>
</Grid.ColumnDefinitions>
<ListView x:Name="ItemList" Grid.Column="0" Margin="2" ItemsSource="{Binding ParentItems}" SelectedItem="{Binding SelectedParentItem, Mode=OneWayToSource}" SelectionMode="Single">
<ListView.ItemTemplate>
<DataTemplate>
<TextBlock Text="{Binding Name}"/>
</DataTemplate>
</ListView.ItemTemplate>
</ListView>
<Grid Grid.Column="1">
<Grid.RowDefinitions>
<RowDefinition Height="Auto"/>
<RowDefinition Height="1*"/>
<RowDefinition Height="Auto"/>
</Grid.RowDefinitions>
<Grid Grid.Row="0">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="1*"/>
<ColumnDefinition Width="1*"/>
<ColumnDefinition Width="1*"/>
<ColumnDefinition Width="1*"/>
</Grid.ColumnDefinitions>
<TextBlock Grid.Column="0" Text="{Binding ElementName=ItemList, Path=SelectedItem.Name}" Margin="2"/>
<TextBlock Grid.Column="1" Text="{Binding ElementName=ItemList, Path=SelectedItem.CreatedOn}" Margin="2"/>
<TextBlock Grid.Column="2" Text="{Binding ElementName=ItemList, Path=SelectedItem.IsActive}" Margin="2"/>
<TextBlock Grid.Column="3" Text="{Binding ElementName=ItemList, Path=SelectedItem.Status}" Margin="2"/>
</Grid>
<ListView Grid.Row="1" Margin="2" ItemsSource="{Binding SelectedParentItem.ChildItems}" />
<Grid Grid.Row="2">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="1*"/>
<ColumnDefinition Width="Auto"/>
</Grid.ColumnDefinitions>
<TextBox x:Name="ChildFilterInput" Grid.Column="0" Margin="2">
<TextBox.InputBindings>
<KeyBinding Command="{Binding FilterChildrenCommand}" CommandParameter="{Binding ElementName=ChildFilterInput, Path=Text}" Key="Return" />
</TextBox.InputBindings>
</TextBox>
<Button Grid.Column="1" Content="Filter" Width="100" Margin="2" Command="{Binding FilterChildrenCommand}" CommandParameter="{Binding ElementName=ChildFilterInput, Path=Text}"/>
</Grid>
</Grid>
</Grid>
</Window>
ViewModelBase.cs:
public abstract class ViewModelBase : INotifyPropertyChanged
{
public event PropertyChangedEventHandler PropertyChanged;
protected bool SetProperty<T>(ref T field, T newValue, [CallerMemberName] string propertyName = null)
{
if (!EqualityComparer<T>.Default.Equals(field, newValue))
{
field = newValue;
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
return true;
}
return false;
}
}
RelayCommand.cs:
public class RelayCommand : ICommand
{
private Predicate<object> _canExecuteMethod;
private Action<object> _executeMethod;
public RelayCommand(Action<object> executeMethod, Predicate<object> canExecuteMethod = null)
{
_executeMethod = executeMethod;
_canExecuteMethod = canExecuteMethod;
}
public event EventHandler CanExecuteChanged
{
add { CommandManager.RequerySuggested += value; }
remove { CommandManager.RequerySuggested -= value; }
}
public bool CanExecute(object parameter)
{
return _canExecuteMethod == null ? true : _canExecuteMethod(parameter);
}
public void Execute(object parameter)
{
_executeMethod(parameter);
}
}
ParentItem.cs:
public class ParentItem
{
public string Name { get; set; }
public List<string> ChildItems { get; set; }
public DateTime CreatedOn { get; set; }
public bool IsActive { get; set; }
public ParentItemStatus Status { get; set; }
}
public enum ParentItemStatus
{
Status_One,
Status_Two
}
App.xaml:
<Application x:Class="FilteringDemo.App"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="clr-namespace:FilteringDemo"
StartupUri="Views/MainWindow.xaml">
<Application.Resources>
</Application.Resources>
</Application>
Note: The MainWindow.xaml file was moved into a "Views" folder, so I'm including the App.xaml with the updated StartupUri in case anyone is trying to copy and paste.

GridView Binding doesn't work. What am I missing?

GridView Binding doesn't work. Could you check what I am missing ?
I'm posting all my code for your convinience.
The purpose of this test code is that if I click add button, person class will be added to the gridview.
C# code below...
public partial class MainWindow : Window
{
List<Person> persons;
public MainWindow()
{
InitializeComponent();
persons = new List<Person>() { new Person() {Name="A", Age=20},
new Person() {Name="B", Age=30}};
lstView.ItemsSource = persons;
}
private void Add_Click(object sender, RoutedEventArgs e)
{
persons.Add(new Person { Name = tbName.Text, Age = Int32.Parse(tbAge.Text) });
}
}
public class Person : INotifyPropertyChanged
{
private string name;
public string Name
{
get { return name; }
set
{
name = value;
OnPropertyChanged("Name");
}
}
private int age;
public int Age
{
get { return age; }
set
{
age = value;
OnPropertyChanged("Age");
}
}
public event PropertyChangedEventHandler PropertyChanged;
void OnPropertyChanged(string name)
{
if (PropertyChanged != null)
PropertyChanged(this, new PropertyChangedEventArgs(name));
}
}
And xaml code..
<Window x:Class="WpfApplication1.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Title="MainWindow" Width="525" Height="150">
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="50"/>
<ColumnDefinition/>
<ColumnDefinition/>
</Grid.ColumnDefinitions>
<Grid.RowDefinitions>
<RowDefinition Height="30"/>
<RowDefinition Height="30"/>
<RowDefinition Height="30"/>
<RowDefinition Height="30"/>
</Grid.RowDefinitions>
<TextBlock Grid.Column="0">Name</TextBlock>
<TextBox Grid.Column="1" x:Name="tbName"></TextBox>
<TextBlock Grid.Column="0" Grid.Row="1">Age</TextBlock>
<TextBox Grid.Column="1" Grid.Row="1" x:Name="tbAge"></TextBox>
<Button Grid.Column="0" Grid.Row="2" Grid.ColumnSpan="2" Click="Add_Click"> Add </Button>
<ListView Grid.Column="2" Grid.RowSpan="4" x:Name="lstView">
<ListView.View>
<GridView>
<GridView.Columns>
<GridViewColumn Header="Name" DisplayMemberBinding="{Binding Name}"/>
<GridViewColumn Header="Age" DisplayMemberBinding="{Binding Age}"/>
</GridView.Columns>
</GridView>
</ListView.View>
</ListView>
</Grid>
INotifyPropertyChanged is for changes on the object (in your case every Person instance).
You need an ObservableCollection (which implements INotifyCollectionChanged) to listen to changes in a collection (in your case new and deleted Person instances in the collection).
Represents a dynamic data collection that provides notifications when
items get added, removed, or when the whole list is refreshed.
more info here
You need an ObservableCollection not a List
ObservableCollection<Person> persons;

Bind a collection of a model to only a part of another collection

I have the requirement to select/map only a few of datasources for each of my applications, by first selecting the application on the left then the mapped/selected (few) datasources on the right (see screenshot).
I often have such a scenario as a business requirement, however I wasn't able to come up with a good solution to model this using WPF bindings.
Given the following code:
The models:
class DataSource
{
public DataSource(string name)
{
Name = name;
}
public string Name { get; private set; }
}
class SoftwareSolution
{
public SoftwareSolution(string name)
{
Name = name;
this.DataSources = new ObservableCollection<DataSource>();
}
public string Name { get; private set; }
public ObservableCollection<DataSource> DataSources { get; private set; }
}
class MainWindowViewModel
{
public MainWindowViewModel()
{
this.SoftwareSolutions = new ObservableCollection<SoftwareSolution>();
this.SoftwareSolutions.Add(new SoftwareSolution("Software 1"));
this.SoftwareSolutions.Add(new SoftwareSolution("Software 2"));
this.SoftwareSolutions.Add(new SoftwareSolution("Software 3"));
this.AllAvailableDatasources = new ObservableCollection<DataSource>();
this.AllAvailableDatasources.Add(new DataSource("SQL Server"));
this.AllAvailableDatasources.Add(new DataSource("Oracle"));
this.AllAvailableDatasources.Add(new DataSource("DB2"));
this.AllAvailableDatasources.Add(new DataSource("MySQL"));
}
public ObservableCollection<DataSource> AllAvailableDatasources { get; private set; }
public ObservableCollection<SoftwareSolution> SoftwareSolutions { get; private set; }
}
XAML:
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="1*"/>
<ColumnDefinition Width="1*"/>
</Grid.ColumnDefinitions>
<TextBlock Text="Application" />
<ListBox Margin="0,25,5,0" VerticalAlignment="Stretch" ItemsSource="{Binding SoftwareSolutions}" DisplayMemberPath="Name" />
<TextBlock Grid.Column="1" Text="DataSources" />
<ListBox Grid.Column="1" Margin="0,25,0,0" VerticalAlignment="Stretch" ItemsSource="{Binding AllAvailableDatasources}">
<ListBox.ItemTemplate>
<DataTemplate>
<DockPanel>
<!-- how should I bind this checkbox? -->
<CheckBox DockPanel.Dock="Left" />
<TextBlock DockPanel.Dock="Left" Text="{Binding Name}" Margin="5,0,0,0" />
</DockPanel>
</DataTemplate>
</ListBox.ItemTemplate>
</ListBox>
</Grid>
The GUI:
What pattern can I use to "bind" the checkboxes on the right to the selection on the right?
You need a DataSourceVM which has a DataSource and an IsSelected property. Each SoftwareSolution has its own collection of DataSourceVMs, and the checkboxes are bound to IsSelected.
public SoftwareSolution(string name, IEnumerable<DataSource> dataSources)
{
Name = name;
this.DataSources = new ObservableCollection<DataSourceVM>(dataSource.Select(x => new DataSourceVM(x));
}
You should filter your ObservableCollection using an ICollectionView, which is the standard approach in these kind of scenarios.
In particular you'll have an event wired on the left which will change the ICollectionView and that will be bound on the column on the right.
More infos about the ICollectionView here: https://msdn.microsoft.com/en-us/library/system.componentmodel.icollectionview%28v=vs.110%29.aspx

MVVM Creating the ViewModel

Can somebody explain to me how exactly to create a ViewModel for the MVVM Pattern.
I tried to understand the the tutorial here: http://msdn.microsoft.com/en-us/magazine/dd419663.aspx , but I was unable to understand what exactly is happening in the code.
Let's say we want to create a basic application about getting and adding people from and to a local database and displaying them in the View. How should the ViewModel look like and how to create the RelayCommands for it. First why do we set the variables twice: once privately and then again publicaly.
EDIT: Thanks for the help so far. I have one more thing that I don't know to do - how to bind the View to the ViewModel and Vice Versa
Here is the Model:
public class Student : INotifyPropertyChanged
{
public event PropertyChangedEventHandler PropertyChanged;
private void OnPropertyChanged(string propertyName)
{
if (PropertyChanged != null)
{
PropertyChanged(this, new PropertyChangedEventArgs(propertyName));
}
}
private string name;
private string surname;
private string age;
public string Name
{
get
{
return name;
}
set
{
name = value;
OnPropertyChanged("Name");
}
}
public string Surname
{
get
{
return surname;
}
set
{
surname = value;
OnPropertyChanged("Surname");
}
}
public string Age
{
get
{
return age;
}
set
{
age = value;
OnPropertyChanged("Age");
}
}
}
and here is the ViewModel:
public class MainViewModel : ViewModelBase
{
ObservableCollection<Student> studentList;
Student selectedPerson;
public MainViewModel()
{
//populate some sample data
studentList = new ObservableCollection<Student>()
{
new Student(){Name="John", Surname="Smith", Age="28"},
new Student(){Name="Barbara", Surname="Anderson", Age="23"}
};
}
public ObservableCollection<Student> StudentList
{
get { return studentList; }
}
public Student SelectedPerson
{
get { return selectedPerson; }
set
{
selectedPerson = value;
RaisePropertyChanged("SelectedPerson");
}
}
private RelayCommand _addStudentCommand;
public ICommand AddStudentCommand
{
get
{
return _addStudentCommand
?? (_addStudentCommand = new RelayCommand(() =>
{
Student student = new Student();
studentList.Add(student);
}));
}
}
}
I have found a way to bind the ViewModel to the View using some code for the view in Csharp but the question how to bind the View to the ViewModel is still on my mind. To be more specific how to create a new student using the values a user has entered in the View.
Here is the View's XAML code
<Window x:Class="MVVMLight.View.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Title="MainWindow"
SizeToContent="WidthAndHeight">
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="2*"/>
<RowDefinition Height="2*"/>
<RowDefinition Height="2*"/>
<RowDefinition Height="*"/>
<RowDefinition Height="Auto"/>
<RowDefinition Height="*"/>
<RowDefinition Height="Auto"/>
</Grid.RowDefinitions>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto"/>
<ColumnDefinition Width="*"/>
</Grid.ColumnDefinitions>
<TextBlock x:Name="NameTextBlock"
Text="Name"
Style="{StaticResource TextBlockTextStyle}"/>
<TextBlock x:Name="SurnameTextBlock"
Grid.Row="1"
Text="Surname"
Style="{StaticResource TextBlockTextStyle}"/>
<TextBlock x:Name="AgeTextBlock"
Grid.Row="2"
Text="Age"
Style="{StaticResource TextBlockTextStyle}"/>
<TextBox x:Name="NameTextBox"
Grid.Column="1"
Style="{StaticResource TextBoxTextStyle}"/>
<TextBox x:Name="SurnameTextBox"
Grid.Row="1"
Grid.Column="1"
Style="{StaticResource TextBoxTextStyle}"/>
<TextBox x:Name="AgeTextBox"
Grid.Row="2"
Grid.Column="1"
Style="{StaticResource TextBoxTextStyle}"/>
<ListBox x:Name="StudentListBox"
Grid.ColumnSpan="2"
Grid.Row="4"
Style="{StaticResource ListBoxStyle}"
ItemsSource="{Binding StudentList}">
<ListBox.ItemTemplate>
<DataTemplate>
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto"/>
<ColumnDefinition Width="Auto"/>
<ColumnDefinition Width="Auto"/>
</Grid.ColumnDefinitions>
<TextBlock Text="{Binding Name}"
Style="{StaticResource TextBlockTextStyle}"/>
<TextBlock Text="{Binding Surname}"
Grid.Column="1"
Style="{StaticResource TextBlockTextStyle}"/>
<TextBlock Text="{Binding Age}"
Grid.Column="2"
Style="{StaticResource TextBlockTextStyle}"/>
</Grid>
</DataTemplate>
</ListBox.ItemTemplate>
</ListBox>
<Button x:Name="AddButton"
Grid.Row="7"
Grid.ColumnSpan="2"
HorizontalAlignment="Center"
Content="Add"
Margin="7,7,7,7"
Command="{Binding AddStudentCommand}"/>
</Grid>
And here is the View's Csharp code
public partial class MainWindow : Window
{
public MainWindow()
{
InitializeComponent();
DataContext = new MainViewModel();
}
}
I have some questions concerning the Binding between the View and The ViewModel:
What are the pros and cons of using this type of binding?
What is the best way of binding if I am going to use a database?
Is this how the ViewModel and Model should look like
How to create a RelayCommand for adding a student to the ObservableCollection
Why do we set things first privately and then again publically [Answered]
How to bind the View to the ViewModel and Vice Versa
in your property setters you should check to see if the new value is equal to the old value, if it is you should return and not fire the PropertyChanged event.
As for your questions:
Yes this looks fine.
There are a couple of ways to setup your relay commands. I prefer
private RelayCommand<Student> _addStudentCommand;
public ICommand AddStudentCommand
{
get
{
return _addStudentCommand
?? (_addStudentCommand = new RelayCommand<Student>((student) =>
{
studentList.Add(student);
}));
}
}
another way without passing in a student object
private RelayCommand _addStudentCommand;
public ICommand AddStudentCommand
{
get
{
return _addStudentCommand
?? (_addStudentCommand = new RelayCommand(() =>
{
Student student = new Student();
studentList.Add(student);
}));
}
}
That is how properties work in .net, You could use automatic properties, but since you need to fire change notification in the setter you have to declare the field that the property will work against.
Also since it looks like you are using mvvm light you should try the code snippets. They make properties very easy to create. type mvvvminpc then hit tab twice. then fill in the highlighted part and hit tab till you are finished.
You can bind the View To the Viewmodel a couple of ways. I know that it is an Antipattern but you could use a locator. The basic idea is to set the viewmodel as the views datacontext.
public class Locator
{
public Viewmodel1 Viewmodel1
{
return new Viewmodel1();
}
}
You then in you app.xaml you add this class
<Application.Resources>
<Locator x:key="VMLocator" />
</Application.Resources>
Then in your view in the xaml
<Page DataContext="{Binding Source="{StaticResource VMLocator}" Path=ViewModel1}">
</Page>

Categories