MVVM: How to get the view reflecting changes in model(custom collection) - wpf

I am experimenting with WPF and MVVM.
My ViewModel contains a custom collection (IEnumerable) which has to be rendered in UI. I have added some code to add a new entity to the Model which I suppose to be rendered in UI. But that does not happen.
[1] Am I wrong with the pattern? [2] Why isn't the new entity is not reflected in the View, though I raise INPC?
Please find my code below
Classes.cs
using System;
using System.Collections;
using System.Collections.Generic;
using System.ComponentModel;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace _06_MVVMTest___Add_to_Model_Reflects_in_View
{
public class Employee
{
public int Id { get; set; }
public string Name { get; set; }
public int Age { get; set; }
public int Salary { get; set; }
public Employee(int id, string name,int age, int salary)
{
this.Id = id;
this.Name = name;
this.Age = age;
this.Salary = salary;
}
}
public class EmployeeCollection : IEnumerable
{
List<Employee> employees = new List<Employee>();
public EmployeeCollection()
{
employees.Add(new Employee(100, "Alice", 23, 300));
employees.Add(new Employee(100, "Bob", 22, 400));
employees.Add(new Employee(100, "Trude", 21, 200));
}
public IEnumerator GetEnumerator()
{
foreach (Employee emp in employees)
{
if (emp==null)
{
break;
}
yield return emp;
}
}
public void AddNewEmployee()//For Test
{
employees.Add(new Employee(200, "Dave", 21, 2000));
}
}
public class EmployeeViewModel:INotifyPropertyChanged
{
EmployeeCollection employees=new EmployeeCollection();
public EmployeeCollection Employees
{
get { return employees; }
}
public event PropertyChangedEventHandler PropertyChanged;
public void NotifyOnEmployeeCollectionChanged()
{
if (PropertyChanged!=null)
PropertyChanged(this,new PropertyChangedEventArgs("Employees"));
}
}
}
MainWindow.xaml.cs
sing System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Data;
using System.Windows.Documents;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Media.Imaging;
using System.Windows.Navigation;
using System.Windows.Shapes;
namespace _06_MVVMTest___Add_to_Model_Reflects_in_View
{
/// <summary>
/// Interaction logic for MainWindow.xaml
/// </summary>
public partial class MainWindow : Window
{
public MainWindow()
{
InitializeComponent();
}
private void Button_Click(object sender, RoutedEventArgs e)
{
EmployeeViewModel vm =(EmployeeViewModel) this.DataContext;
vm.Employees.AddNewEmployee();
vm.NotifyOnEmployeeCollectionChanged();
}
}
}
MainWindow.xaml
<Window x:Class="_06_MVVMTest___Add_to_Model_Reflects_in_View.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="clr-namespace:_06_MVVMTest___Add_to_Model_Reflects_in_View"
Title="Employee Window" Height="350" Width="525">
<Window.DataContext>
<local:EmployeeViewModel />
</Window.DataContext>
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="*"/>
<RowDefinition Height="80"/>
</Grid.RowDefinitions>
<Grid.ColumnDefinitions>
<ColumnDefinition />
<ColumnDefinition />
</Grid.ColumnDefinitions>
<ListView Height="300" Grid.Row="0" Grid.Column="0" BorderBrush="Blue" ItemsSource="{Binding Employees}">
<ListView.ItemTemplate>
<DataTemplate>
<Grid Background="LightCoral" Margin="10,10,10,10">
<StackPanel>
<TextBlock Text="{Binding Name}"/>
<TextBlock Text="{Binding Age}"/>
</StackPanel>
</Grid>
</DataTemplate>
</ListView.ItemTemplate>
</ListView>
<ListView Height="300" Grid.Row="0" Grid.Column="1" BorderBrush="Blue" ItemsSource="{Binding Employees}">
<ListView.ItemTemplate>
<DataTemplate>
<Grid Background="LightCoral" Margin="10,10,10,10">
<StackPanel>
<TextBlock Text="{Binding Name}"/>
<TextBlock Text="{Binding Age}"/>
</StackPanel>
</Grid>
</DataTemplate>
</ListView.ItemTemplate>
</ListView>
<Button Grid.Row="1" Height="40" Content="add new employee" Click="Button_Click" />
</Grid>
</Window>

There is a problem with the way WPF handles property changes along with your way of notifying the change. WPF checks to see if your value is still the same; the EmployeeCollection is actually the same as the old one (it is still the same instance and it won't look inside the collection).
One solution is to change the implementation of EmployeeCollection to this:
public class EmployeeCollection : IEnumerable, INotifyCollectionChanged
{
private ObservableCollection<Employee> employees = new ObservableCollection<Employee>();
public EmployeeCollection()
{
employees.Add(new Employee(100, "Alice", 23, 300));
employees.Add(new Employee(100, "Bob", 22, 400));
employees.Add(new Employee(100, "Trude", 21, 200));
}
public IEnumerator GetEnumerator()
{
return employees.GetEnumerator();
}
public void AddNewEmployee() //For Test
{
employees.Add(new Employee(200, "Dave", 21, 2000));
}
public event NotifyCollectionChangedEventHandler CollectionChanged
{
add { employees.CollectionChanged += value; }
remove { employees.CollectionChanged -= value; }
}
}
This introduces the INotifyCollectionChanged interface and removes the need to call vm.NotifyOnEmployeeCollectionChanged() in the main window, since the ObservableCollection<T> underlying the EmployeeCollection will tell WPF that it has changed.
If you do not want to learn about these concepts for now, a quick work around would be to :
public void NotifyOnEmployeeCollectionChanged()
{
var current = this.employees;
this.employees = null;
if (PropertyChanged != null)
PropertyChanged(this, new PropertyChangedEventArgs("Employees"));
this.employees = current;
if (PropertyChanged != null)
PropertyChanged(this, new PropertyChangedEventArgs("Employees"));
}

Your custom collection should implement INotifyCollectionChanged. Every time you add an item, fire the CollectionChanged event.
public class EmployeeCollection : IEnumerable, INotifyCollectionChanged
{
public event NotifyCollectionChangedEventHandler CollectionChanged;
public void AddNewEmployee() //For Test
{
employees.Add(new Employee(200, "Dave", 21, 2000));
if (CollectionChanged != null)
CollectionChanged(this, new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Add));
}

Related

Binding to a Controls DependencyProperty in DataTemplate of Listbox with dynamic items does not work

Ok, since I spend one day reading articles in stackoverflow without success I must ask for help. Complete example follows below.
Short version:
I have a DoubleTextBox, which provides a DependencyProperty "Number". When I use the DoubleTextBox directly, the Binding and Validation works fine. However, when I use it in a DataTemplate of a Listbox, the Binding to Objects in an ObservableCollection does fail. ValidatesOnDataErrors works neither.
Thank you very, very much!
The XAML:
<Window x:Class="WpfApp1.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:WpfApp1"
mc:Ignorable="d"
Title="MainWindow" Height="350" Width="525">
<Window.DataContext>
<local:ViewModel x:Name="ViewModel"/>
</Window.DataContext>
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition/>
<ColumnDefinition Width="5"/>
<ColumnDefinition/>
</Grid.ColumnDefinitions>
<Grid Grid.Column="0">
<StackPanel Margin="10">
<StackPanel Orientation="Horizontal">
<TextBlock Text="Time (s):" Width="60"/>
<local:DoubleTextBox Width="60"
Number="{Binding Time, UpdateSourceTrigger=PropertyChanged,
ValidatesOnDataErrors=True}" Decimals="2"/>
</StackPanel>
</StackPanel>
</Grid>
<Grid Grid.Column="1" Background="SteelBlue"/>
<Grid Grid.Column="2">
<Grid.Resources>
<DataTemplate x:Key="MethodProgramViewTemplate">
<Grid KeyboardNavigation.TabNavigation="Local">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="60"/>
<ColumnDefinition Width="60"/>
</Grid.ColumnDefinitions>
<TextBlock Grid.Column="0" Text="Flow"/>
<local:DoubleTextBox Grid.Column="1" TabIndex="0" Number="{Binding Flow, UpdateSourceTrigger=PropertyChanged, ValidatesOnDataErrors=True}" Decimals="2" />
</Grid>
</DataTemplate>
</Grid.Resources>
<ListBox x:Name="PumpProgramListBox"
KeyboardNavigation.TabNavigation="Local"
ItemsSource="{Binding Path=MethodProgramView}"
ItemTemplate="{StaticResource MethodProgramViewTemplate}">
<ListBox.ItemContainerStyle>
<Style TargetType="{x:Type ListBoxItem}">
<Setter Property="HorizontalContentAlignment" Value="Stretch"/>
<Setter Property="VerticalContentAlignment" Value="Stretch"/>
</Style>
</ListBox.ItemContainerStyle>
</ListBox>
</Grid>
</Grid>
</Window>
The ViewModel:
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.ComponentModel;
using System.Linq;
using System.Text;
using System.Windows.Data;
namespace WpfApp1
{
public class ViewModel : INotifyPropertyChanged, IDataErrorInfo
{
private double time = 0.0;
public double Time
{
get => this.time;
set
{
if (this.time != value)
{
System.Diagnostics.Debug.WriteLine(String.Format("ViewModel Time - from {0} to {1}", this.time, value));
this.time = value;
RaisePropertyChanged(nameof(Time));
}
}
}
public ObservableCollection<Method> methodProgram { get; private set; } = new ObservableCollection<Method>();
private CollectionViewSource methodProgramView;
public ICollectionView MethodProgramView
{
get
{
if (methodProgramView == null)
{
methodProgramView = new CollectionViewSource();
methodProgramView.Source = methodProgram;
}
return methodProgramView.View;
}
}
public ViewModel()
{
this.Time = 1.5;
for (int i = 1; i <= 3; i++)
{
this.methodProgram.Add(new Method() { Flow = i });
}
}
public event PropertyChangedEventHandler PropertyChanged;
internal void RaisePropertyChanged(string propName) =>
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propName));
public string Error => "";
public string this[string columnName]
{
get
{
string result = null;
switch (columnName)
{
case nameof(Time):
result = this.Time < 0 ? "Time must be positive" : null;
break;
default:
break;
}
return result;
}
}
}
}
The "Method" Object:
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Linq;
using System.Text;
namespace WpfApp1
{
public class Method : INotifyPropertyChanged, IDataErrorInfo
{
private double flow = 0.0;
public double Flow
{
get => this.flow;
set
{
if (this.flow != value)
{
System.Diagnostics.Debug.WriteLine(String.Format("Method Flow - from {0} to {1}", this.flow, value));
this.flow = value;
RaisePropertyChanged(nameof(Flow));
}
}
}
public event PropertyChangedEventHandler PropertyChanged;
internal void RaisePropertyChanged(string propName) =>
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propName));
public string Error => "";
public string this[string columnName]
{
get
{
string result = null;
switch (columnName)
{
case nameof(Flow):
result = this.Flow < 0 ? "Flow must be positive" : null;
break;
default:
break;
}
return result;
}
}
}
}
The DoubleTextBox:
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Text.RegularExpressions;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Data;
using System.Windows.Input;
namespace WpfApp1
{
public class DoubleTextBox : TextBox
{
#region Properties
public double Number
{
get { return (double)this.GetValue(NumberProperty); }
set { this.SetValue(NumberProperty, value); }
}
public static readonly DependencyProperty NumberProperty = DependencyProperty.Register(
nameof(Number), typeof(double), typeof(DoubleTextBox),
new FrameworkPropertyMetadata(3.3d, FrameworkPropertyMetadataOptions.BindsTwoWayByDefault,
new PropertyChangedCallback(NumberProperty_PropertyChanged))
);
public int Decimals
{
get { return (int)this.GetValue(DecimalsProperty); }
set { this.SetValue(DecimalsProperty, value); }
}
public static readonly DependencyProperty DecimalsProperty = DependencyProperty.Register(
nameof(Decimals), typeof(int), typeof(DoubleTextBox),
new FrameworkPropertyMetadata((int)2, FrameworkPropertyMetadataOptions.BindsTwoWayByDefault,
new PropertyChangedCallback(DecimalsProperty_PropertyChanged))
);
#endregion Properties
#region Constructor
public DoubleTextBox()
{
this.TextChanged += DoubleTextBox_TextChanged;
this.LostFocus += DoubleTextBox_LostFocus;
this.PreviewTextInput += DoubleTextBox_PreviewTextInput;
this.RefreshText();
}
#endregion Constructor
#region Methods
private static void NumberProperty_PropertyChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
System.Diagnostics.Debug.WriteLine(String.Format("NumberProperty_PropertyChanged - from {0} to {1}", e.OldValue, e.NewValue));
(d as DoubleTextBox).RefreshText();
}
private static void DecimalsProperty_PropertyChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
(d as DoubleTextBox).RefreshText();
}
private void RefreshText()
{
System.Diagnostics.Debug.WriteLine(String.Format("DoubleTextBox - DataContext: {0}", this.DataContext));
Text = Number.ToString("N" + this.Decimals.ToString());
}
private void DoubleTextBox_PreviewTextInput(object sender, TextCompositionEventArgs e)
{
e.Handled = !IsTextAllowed(e.Text);
}
private static bool IsTextAllowed(string text)
{
string CultureName = System.Threading.Thread.CurrentThread.CurrentCulture.Name;
System.Globalization.CultureInfo ci = new System.Globalization.CultureInfo(CultureName);
char DecimalSeparator = ci.NumberFormat.NumberDecimalSeparator[0];
Regex regex = new Regex(String.Format("([-+0-9]+[{0}][0-9]+)|([-+0-9{0}])", DecimalSeparator)); //regex that matches only numbers
bool match = regex.IsMatch(text);
return regex.IsMatch(text);
}
void DoubleTextBox_LostFocus(object sender, System.Windows.RoutedEventArgs e)
{
RefreshText();
}
void DoubleTextBox_TextChanged(object sender, TextChangedEventArgs e)
{
double number;
if (string.IsNullOrEmpty(Text))
{
Number = 0;
}
else if (double.TryParse(Text, out number))
{
Number = Double.Parse(number.ToString("N" + this.Decimals.ToString()));
}
else
{
BindingExpression bex = GetBindingExpression(NumberProperty);
if (bex != null)
{
ValidationError validationError = new ValidationError(new ExceptionValidationRule(), bex);
validationError.ErrorContent = "Number is not valid";
Validation.MarkInvalid(bex, validationError);
}
}
}
#endregion Methods
}
}
The call of RefreshText() in the constructor of DoubleTextBox has to be removed. Still don't know why, but it works.
Thanks to #user1481065 and this issue: WPF Binding to DependencyProperty of UserControl in DataTemplate is not working

how to insert update delete operations on wpf mvvm using list?

Here i have written the update operation, and also i would like to know how to perform insert and delete opearations on MVVM?
View section:
filename: Person.xaml----> it is UI designing file
<Window x:Class="WpfApplication5.View.Person"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Title="Person" Height="300" Width="300"
xmlns:y="clr-namespace:WpfApplication5.ViewModel">
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="auto"/>
<RowDefinition Height="auto"/>
<RowDefinition Height="*"/>
</Grid.RowDefinitions>
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="100"></ColumnDefinition>
<ColumnDefinition Width="*"></ColumnDefinition>
</Grid.ColumnDefinitions>
<Grid.RowDefinitions>
<RowDefinition Height="auto"></RowDefinition>
<RowDefinition Height="auto"></RowDefinition>
</Grid.RowDefinitions>
<Label x:Name="lblName" Content="Name" Grid.Row="0" Grid.Column="0" VerticalAlignment="Top"></Label>
<TextBox x:Name="txtName" Grid.Row="0" Grid.Column="1" VerticalAlignment="Top" Text="{Binding ElementName=lstPerson, Path=SelectedItem.Name}"></TextBox>
<Label x:Name="lblAddress" Content="Address" Grid.Row="1" Grid.Column="0" VerticalAlignment="Top"></Label>
<TextBox x:Name="txtAddress" Grid.Row="1" Grid.Column="1" VerticalAlignment="Top" Text="{Binding ElementName=lstPerson, Path=SelectedItem.Address}"></TextBox>
</Grid>
<Button x:Name="btnUpdate" Width="100" Height="20" HorizontalAlignment="Center" Grid.Row="1" Content="Update"
Command="{Binding Path=UpdateCommand}" CommandParameter="{Binding ElementName=lstPerson, Path=SelectedItem.Address}"></Button>
<ListView x:Name="lstPerson" Grid.Row="2" ItemsSource="{Binding Persons}">
<ListView.View>
<GridView>
<GridViewColumn Header="Name" DisplayMemberBinding="{Binding Name}" />
<GridViewColumn Header="Address" Width="200" DisplayMemberBinding="{Binding Address}"/>
</GridView>
</ListView.View>
</ListView>
</Grid>
</Window>
VieModel section:
Filename is: PersonViewModel.cs
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Windows.Input;
using WpfApplication5.Model;
namespace WpfApplication5.ViewModel
{
class PersonViewModel
{
private IList<Person> _personList;
public PersonViewModel()
{
_personList = new List<Person>()
{
new Person(){Name="Prabhat", Address="Bangalore"},
new Person(){Name="John",Address="Delhi"}
};
}
public IList<Person> Persons
{
get { return _personList; }
set { _personList = value; }
}
private ICommand mUpdater;
public ICommand UpdateCommand
{
get
{
if (mUpdater == null)
mUpdater = new Updater();
return mUpdater;
}
set
{
mUpdater = value;
}
}
}
class Updater : ICommand
{
#region ICommand Members
public bool CanExecute(object parameter)
{
return true;
}
public event EventHandler CanExecuteChanged
{
add { CommandManager.RequerySuggested += value; }
remove { CommandManager.RequerySuggested -= value; }
}
public void Execute(object parameter)
{
//Your Code
}
#endregion
}
}
Model section:
filename is:Person.cs
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace WpfApplication5.Model
{
class Person : INotifyPropertyChanged
{
private string name;
private string address;
public event PropertyChangedEventHandler PropertyChanged;
public void OnPropertyChanged(string propertyName)
{
if (PropertyChanged != null)
{
PropertyChanged(this, new PropertyChangedEventArgs(propertyName));
}
}
public string Name
{
get
{
return name;
}
set
{
name = value;
OnPropertyChanged("Name");
}
}
public string Address
{
get
{
return address;
}
set
{
address = value;
OnPropertyChanged("Address");
}
}
}
}
Instead of using List<T>, use ObservableCollection for source collection.
Reason being it implements INotifyCollectionChanged which means any insert/add/delete operation to this list will refresh bounded UI automatically. You need not to worry about raising collection changed events.
private ObservableCollection<Person> _personList;
public ObservableCollection<Person> Persons
{
get { return _personList; }
set { _personList = value; }
}
Moreover, instead of creating separate class for each implementation of ICommand, consider using generic version of RelayCommand or DelegateCommand.
RelayCommand:
public class RelayCommand<T> : ICommand
{
#region Fields
readonly Action<T> _execute = null;
readonly Predicate<T> _canExecute = null;
#endregion
#region Constructors
/// <summary>
/// Initializes a new instance of <see cref="DelegateCommand{T}"/>.
/// </summary>
/// <param name="execute">Delegate to execute when Execute is called on the command. This can be null to just hook up a CanExecute delegate.</param>
/// <remarks><seealso cref="CanExecute"/> will always return true.</remarks>
public RelayCommand(Action<T> execute)
: this(execute, null)
{
}
/// <summary>
/// Creates a new command.
/// </summary>
/// <param name="execute">The execution logic.</param>
/// <param name="canExecute">The execution status logic.</param>
public RelayCommand(Action<T> execute, Predicate<T> canExecute)
{
if (execute == null)
throw new ArgumentNullException("execute");
_execute = execute;
_canExecute = canExecute;
}
#endregion
#region ICommand Members
///<summary>
///Defines the method that determines whether the command can execute in its current state.
///</summary>
///<param name="parameter">Data used by the command. If the command does not require data to be passed, this object can be set to null.</param>
///<returns>
///true if this command can be executed; otherwise, false.
///</returns>
public bool CanExecute(object parameter)
{
return _canExecute == null ? true : _canExecute((T)parameter);
}
///<summary>
///Occurs when changes occur that affect whether or not the command should execute.
///</summary>
public event EventHandler CanExecuteChanged
{
add { CommandManager.RequerySuggested += value; }
remove { CommandManager.RequerySuggested -= value; }
}
///<summary>
///Defines the method to be called when the command is invoked.
///</summary>
///<param name="parameter">Data used by the command. If the command does not require data to be passed, this object can be set to <see langword="null" />.</param>
public void Execute(object parameter)
{
_execute((T)parameter);
}
#endregion
}
Use RelayCommand and create handlers within your ViewModel class like this:
private ICommand mUpdater;
public ICommand UpdateCommand
{
get
{
if (mUpdater == null)
mUpdater = new RelayCommand<object>(Update);
return mUpdater;
}
}
private void Update(object parameter)
{
// Update collection based on the parameter passed from View.
}

Issues with {RelativeSource PreviousData} when removing collection elements

I'm using the following (simplified) code to display an element in all the items in an ItemsControl except the first:
<TheElement Visibility="{Binding RelativeSource={RelativeSource PreviousData},
Converter={StaticResource NullToVisibility}}/>
NullToVisibility is a simple converter that returns Visibility.Hidden if the source is null, Visibility.Visible otherwise.
Now, this works fine when binding the view initially, or adding elements to the list (an ObservableCollection), but the element is not made invisible on the second element when removing the first.
Any ideas on how to fix this?
Had some wasted code leftover from a previous answer... might as well use it here:
The key is to refresh the viewsource e.g. :
CollectionViewSource.GetDefaultView(this.Categories).Refresh();
Full example source below. Remove First Item removes first element and refreshes the view:
RelativeSourceTest.xaml
<UserControl x:Class="WpfApplication1.RelativeSourceTest"
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:PersonTests="clr-namespace:WpfApplication1" mc:Ignorable="d"
d:DesignHeight="300" d:DesignWidth="300">
<UserControl.Resources>
<PersonTests:NullToVisibilityConvertor x:Key="NullToVisibility"/>
</UserControl.Resources>
<Grid>
<StackPanel Background="White">
<Button Content="Remove First Item" Click="Button_Click"/>
<ListBox ItemsSource="{Binding Categories}">
<ListBox.ItemTemplate>
<DataTemplate>
<CheckBox IsChecked="{Binding Checked, Mode=TwoWay}" >
<StackPanel>
<TextBlock Text="{Binding CategoryName}"/>
<TextBlock Text="Not The First"
Visibility="{Binding RelativeSource={RelativeSource PreviousData},
Converter={StaticResource NullToVisibility}}"/>
</StackPanel>
</CheckBox>
</DataTemplate>
</ListBox.ItemTemplate>
</ListBox>
</StackPanel>
</Grid>
</UserControl>
RelativeSourceTest.xaml.cs
using System.Collections.ObjectModel;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Data;
namespace WpfApplication1
{
public partial class RelativeSourceTest : UserControl
{
public ObservableCollection<Category> Categories { get; set; }
public RelativeSourceTest()
{
InitializeComponent();
this.Categories = new ObservableCollection<Category>()
{
new Category() {CategoryName = "Category 1"},
new Category() {CategoryName = "Category 2"},
new Category() {CategoryName = "Category 3"},
new Category() {CategoryName = "Category 4"}
};
this.DataContext = this;
}
private void Button_Click(object sender, RoutedEventArgs e)
{
this.Categories.RemoveAt(0);
CollectionViewSource.GetDefaultView(this.Categories).Refresh();
}
}
}
Category.cs
using System.ComponentModel;
namespace WpfApplication1
{
public class Category : INotifyPropertyChanged
{
public event PropertyChangedEventHandler PropertyChanged;
private bool _checked;
public bool Checked
{
get { return _checked; }
set
{
if (_checked != value)
{
_checked = value;
SendPropertyChanged("Checked");
}
}
}
private string _categoryName;
public string CategoryName
{
get { return _categoryName; }
set
{
if (_categoryName != value)
{
_categoryName = value;
SendPropertyChanged("CategoryName");
}
}
}
public virtual void SendPropertyChanged(string propertyName)
{
if (PropertyChanged != null)
{
PropertyChanged(this, new PropertyChangedEventArgs(propertyName));
}
}
}
}
It's '17 now, but the problem is here. MVVM approach (as I see it):
public class PreviousDataRefreshBehavior : Behavior<ItemsControl> {
protected override void OnAttached() {
var col = (INotifyCollectionChanged)AssociatedObject.Items;
col.CollectionChanged += OnCollectionChanged;
}
protected override void OnDetaching() {
var col = (INotifyCollectionChanged)AssociatedObject.Items;
col.CollectionChanged -= OnCollectionChanged;
}
private void OnCollectionChanged(object sender, NotifyCollectionChangedEventArgs e) {
if(e.Action != NotifyCollectionChangedAction.Remove) {
return;
}
CollectionViewSource.GetDefaultView(AssociatedObject.ItemsSource).Refresh();
}
}
and usage:
<ItemsControl>
<int:Interaction.Behaviors>
<behaviors:PreviousDataRefreshBehavior/>
</int:Interaction.Behaviors>
</ItemsControl>
Underlying CollectionViewSource has to be refreshed after remove.
CollectionViewSource.GetDefaultView(this.Items).Refresh();

Set ComboBox SelectedItem to dataobject not present in ItemSource

I have a combobox whose SelectedItem and ItemSource properties are bound to the viewmodel. The objects in the ItemSource are time sensitive in that the objects expire and I can only set active items in the collection to the ItemSource. Now at a certain point, the selectedItem object maynot be in the ItemSource.
I think the normal behaviour of the ItemSource is to only allow objects to be selected from the ItemSource Collection but in this case I do want to select an object which is no longer in the ItemSource.
How can I implement this behaviour? I will post some code here to support my problem.
Window.xaml
<Window x:Class="ExpiredSelectedItem.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Title="MainWindow" Height="350" Width="525">
<Grid>
<ComboBox Height="23"
HorizontalAlignment="Left"
Margin="184,68,0,0"
Name="comboBox1"
VerticalAlignment="Top"
Width="120"
ItemsSource="{Binding CustomList}"
SelectedItem="{Binding ActiveItem}"
SelectedValue="Code"
SelectedValuePath="{Binding ActiveItem.Code}"
DisplayMemberPath="Code"
/>
</Grid>
Window.xaml.cs
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Data;
using System.Windows.Documents;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Media.Imaging;
using System.Windows.Navigation;
using System.Windows.Shapes;
namespace ExpiredSelectedItem
{
/// <summary>
/// Interaction logic for MainWindow.xaml
/// </summary>
public partial class MainWindow : Window
{
public MainWindow()
{
InitializeComponent();
CustomList = new List<DomainObject>();
CustomList.AddRange(new DomainObject[] {
new DomainObject() { Code = "A", IsActive =true},
new DomainObject() { Code ="B", IsActive = true},
new DomainObject() { Code = "C", IsActive =true},
});
ActiveItem = new DomainObject() { Code = "D", IsActive = false };
this.DataContext = this;
}
public List<DomainObject> CustomList { get; set; }
public DomainObject ActiveItem { get; set; }
}
public class DomainObject
{
public string Code { get; set; }
public bool IsActive { get; set; }
}
}
Even though I select the code D in the code-behind, when the combobox loads the first item (A) is selected. Expected behaviour was that "D" should have been selected in the textbox but no items should be when the dropdown is opened.
I don't know if this plays any role, but it sure looks suspicious. Try removing it:
<ComboBox ... SelectedValue="Code" ...
To keep the selected item when it is not in the list you can to do the following:
Prevent the ActiveItem from getting set to NULL; see: if (value != null)
Clear the ComboBox; see: comboBox1.SelectedIndex = -1;
Implement INotifyPropertyChanged (which you probably already do)
These changes might cause your code more harm than good, but hopefully this gets you started.
Here is the code-behind:
using System.Collections.Generic;
using System.ComponentModel;
using System.Windows;
namespace ComboBoxSelectedItem
{
public partial class MainWindow : Window, INotifyPropertyChanged
{
public event PropertyChangedEventHandler PropertyChanged;
void Notify(string propName)
{
if (PropertyChanged != null)
{
PropertyChanged(this, new PropertyChangedEventArgs(propName));
}
}
public MainWindow()
{
InitializeComponent();
CustomList = new List<DomainObject>();
CustomList.AddRange(new DomainObject[] {
new DomainObject() { Code = "A", IsActive =true},
new DomainObject() { Code ="B", IsActive = true},
new DomainObject() { Code = "C", IsActive =true},
});
this.DataContext = this;
}
public List<DomainObject> CustomList { get; set; }
private DomainObject _activeItem = new DomainObject() { Code = "D", IsActive = false };
public DomainObject ActiveItem
{
get { return _activeItem; }
set
{
if (value != null)
{
_activeItem = value;
}
Notify("ActiveItem");
}
}
private void Button_Click(object sender, RoutedEventArgs e)
{
ActiveItem = new DomainObject() { Code = "D", IsActive = false };
comboBox1.SelectedIndex = -1;
}
}
}
Here is the XAML:
<Window x:Class="ComboBoxSelectedItem.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Title="MainWindow" Height="350" Width="525">
<Grid>
<Grid.RowDefinitions>
<RowDefinition/>
<RowDefinition/>
<RowDefinition/>
</Grid.RowDefinitions>
<ComboBox
Grid.Row="0"
Height="23"
HorizontalAlignment="Left"
Name="comboBox1"
VerticalAlignment="Top"
Width="120"
ItemsSource="{Binding CustomList}"
SelectedItem="{Binding ActiveItem}"
DisplayMemberPath="Code"/>
<TextBox
Grid.Row="1"
Text="{Binding Path=ActiveItem.Code, Mode=TwoWay}"/>
<Button
Grid.Row="2"
Content="D"
Click="Button_Click"/>
</Grid>
</Window>

WPF - UI binding how to ? how come this example code doesn't work

this is my xmal mainwindow :
<Window x:Class="HR1.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Title="MainWindow" Height="350" Width="525">
<Grid>
<StackPanel HorizontalAlignment="Left" Width="162">
<ListView ItemsSource="{Binding EventsCollection}" Background="AliceBlue" Height="311" >
<ListView.View>
<GridView>
<GridViewColumn Header="event Date" Width="112"/>
</GridView>
</ListView.View>
</ListView>
</StackPanel>
<ScrollViewer>
<StackPanel Margin="116,0,0,0" Width="284">
<ListView Name="employeeListView" ItemsSource="{Binding EmployeeCollection}">
<ListView.View>
<GridView>
<GridViewColumn Header="Employee ID" DisplayMemberBinding="{Binding Path=EmployeeID}" Width="80"/>
<GridViewColumn Header="First Name" DisplayMemberBinding="{Binding Path=FirstName}" Width="80"/>
<GridViewColumn Header="Last Name" DisplayMemberBinding="{Binding Path=LastName}" Width="80"/>
<!--<GridViewColumn Header="start" DisplayMemberBinding="{Binding Path=startHR}" Width="67" />
<GridViewColumn Header="finish" DisplayMemberBinding="{Binding Path=finishHR}" Width="67"/>-->
</GridView>
</ListView.View>
</ListView>
</StackPanel>
</ScrollViewer>
</Grid>
where this is the code behind:
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Data;
using System.Windows.Documents;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Media.Imaging;
using System.Windows.Navigation;
using System.Windows.Shapes;
using HR1.Model;
namespace HR1
{
/// <summary>
/// Interaction logic for MainWindow.xaml
/// </summary>
public partial class MainWindow : Window
{
private CollectionView employeeCollection;
private CollectionView eventsCollection;
public MainWindow()
{
InitializeComponent();
initlalizeEmployeeList();
initlalizeEventDateList();
}
private void initlalizeEventDateList()
{
IList<employeeData> list = new List<employeeData>();
//query to database should be here
foreach (employeeData dataString in employeeDatesString)
{
list.Add(dataString );
}
employeeCollection = new CollectionView(list);
}
private void initlalizeEmployeeList()
{
IList<eventDate> list = new List<eventDate>();
//query to database should be here
foreach (eventDate dataString in eventDataString)
{
list.Add(dataString);
}
eventsCollection = new CollectionView(list);
}
#region collection binding setters
public CollectionView EmployeeCollection
{
get { return employeeCollection; }
}
public CollectionView EventsCollection
{
get { return eventsCollection; }
}
#endregion
#region myDatabinding
static employeeData []employeeDatesString = {new employeeData(1234,"yoav","stern "),new employeeData(1234,"yoav","stern ") };
static eventDate[] eventDataString = { new eventDate("111"), new eventDate("2222") };
#endregion
}
}
and this the data i store in tow classes which are stored in two places:
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.ComponentModel;
namespace HR1.Model
{
class eventDate : INotifyPropertyChanged
{
public eventDate(string s)
{
Date = s;
}
#region private members
private string date;
#endregion
#region proprties
public string Date
{
get
{
return this.date;
}
set
{
date = value;
if (value != this.date)
{
this.date = value;
NotifyPropertyChanged("Date");
}
}
}
#endregion
#region events
public event PropertyChangedEventHandler PropertyChanged;
#endregion
private void NotifyPropertyChanged(String info)
{
if (PropertyChanged != null)
{
PropertyChanged(this, new PropertyChangedEventArgs(info));
}
}
}
}
and this the second one :
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.ComponentModel;
namespace HR1.Model
{
class employeeData : INotifyPropertyChanged
{
public employeeData(long ID,string f,string l){
EmployeeID = ID;
LastName = l;
FirstName = f;
}
#region privete Members
private long employeeID;
private string firstName;
private string lastName;
#endregion
#region proprties
public long EmployeeID
{
get
{
return employeeID;
}
set
{
employeeID = value;
if (value != this.employeeID)
{
this.employeeID = value;
NotifyPropertyChanged("EmployeeID");
}
}
}
public string FirstName
{
get
{
return firstName;
}
set
{
firstName = value;
if (value != this.firstName)
{
this.firstName = value;
NotifyPropertyChanged("FirstName");
}
}
}
public string LastName
{
get
{
return lastName;
}
set
{
lastName = value;
if (value != this.lastName)
{
this.lastName = value;
NotifyPropertyChanged("LastName");
}
}
}
#endregion
#region events
public event PropertyChangedEventHandler PropertyChanged;
#endregion
private void NotifyPropertyChanged(String info)
{
if (PropertyChanged != null)
{
PropertyChanged(this, new PropertyChangedEventArgs(info));
}
}
}
}
notice : in my point of view somewhere where i declared
EmployeeCollection
and the xmal line:
<ListView Name="employeeListView" ItemsSource="{Binding EmployeeCollection}">
I did wrong couse the two doesn't bind as they should have
If you change that first <Grid> to:
<Grid DataContext="{Binding ElementName=window}">
and add x:Name="window" to the root Window element, then does it work?
Update your MainWindow constructor to:
public MainWindow()
{
InitializeComponent();
initlalizeEmployeeList();
initlalizeEventDateList();
this.DataContext = this;
}
I found out what the problem is
i nedded to do
NotifyPropertyChanged("somelist");
example :
private void initlalizeEventDateList()
{
IList<employeeData> list = new List<employeeData>();
//query to database should be here
foreach (employeeData dataString in employeeDatesString)
{
list.Add(dataString );
}
employeeCollection = new CollectionView(list);
NotifyPropertyChanged("employeeCollection");
}

Resources