I'm wondering about how to do validation the mvvm way. I saw lots of content on this topic on the web, but nothing seems to cover my situation, but maybe I'm just approaching it the wrong way. I have a ValidableModel base class from which my other models inherit:
public abstract class ValidableModel : IDataErrorInfo
{
protected Type _type;
protected readonly Dictionary<string, ValidationAttribute[]> _validators;
protected readonly Dictionary<string, PropertyInfo> _properties;
public ValidableModel()
{
_type = this.GetType();
_properties = _type.GetProperties().ToDictionary(p => p.Name, p => p);
_validators = _properties.Where(p => _getValidations(p.Value).Length != 0).ToDictionary(p => p.Value.Name, p => _getValidations(p.Value));
}
protected ValidationAttribute[] _getValidations(PropertyInfo property)
{
return (ValidationAttribute[])property.GetCustomAttributes(typeof(ValidationAttribute), true);
}
public string this[string columnName]
{
get
{
if (_properties.ContainsKey(columnName))
{
var value = _properties[columnName].GetValue(this, null);
var errors = _validators[columnName].Where(v => !v.IsValid(value)).Select(v => v.ErrorMessage).ToArray();
Error = string.Join(Environment.NewLine, errors);
return Error;
}
return string.Empty;
}
}
public string Error
{
get;
set;
}
}
public class SomeModelWithManyFields : ValidableModel {
[Required(ErrorMessage = "required stuff")]
public string Stuff { get; set; }
[Required(ErrorMessage = "another required stuff")]
public string OtherStuff { get; set; }
// and so on
}
This is just an example - in reality my models have more fields (obviously :) ). Now, in my ViewModel I'm exposing whole instance of my model. All this seemed natural - if I'd be exposing every field of every model then I'd have lots of duplicated code. Recently I started wondering if I'm approaching this problem correct. Is there a way to validate my models without code duplication, and not by doing this on the model, but on the ViewModel?
Try this,
EntityBase.cs //This class has Validation logic and all the entites you want to validate must inherit this class
[DataContract(IsReference = true)]
[Serializable]
public abstract class EntityBase : INotifyPropertyChanged, IDataErrorInfo
{
#region Fields
//This hold the property name and its value
private Dictionary<string, object> _values = new Dictionary<string, object>();
#endregion Fields
#region Action
//Subscribe this event if want to know valid changed
public event Action IsValidChanged;
#endregion
#region Protected
protected void SetValue<T>(Expression<Func<T>> propertySelector, T value)
{
string propertyName = GetPropertyName(propertySelector);
SetValue(propertyName, value);
}
protected void SetValue<T>(string propertyName, T value)
{
if (string.IsNullOrEmpty(propertyName))
throw new ArgumentException("Invalid property name", propertyName);
_values[propertyName] = value;
NotifyPropertyChanged(propertyName);
if (IsValidChanged != null)
IsValidChanged();
}
protected T GetValue<T>(Expression<Func<T>> propertySelector)
{
string propertyName = GetPropertyName(propertySelector);
return GetValue<T>(propertyName);
}
protected T GetValue<T>(string propertyName)
{
if (string.IsNullOrEmpty(propertyName))
throw new ArgumentNullException("invalid property name",propertyName);
object value;
if (!_values.TryGetValue(propertyName, out value))
{
value = default(T);
_values.Add(propertyName, value);
}
return (T)value;
}
protected virtual string OnValidate(string propertyName)
{
if (string.IsNullOrEmpty(propertyName))
throw new ArgumentNullException("propertyName","invalid property name");
string error = string.Empty;
object value = GetValue(propertyName);
//Get only 2 msgs
var results = new List<ValidationResult>(2);
bool result = Validator.TryValidateProperty(value,new ValidationContext(this, null, null){MemberName = propertyName},results);
//if result have errors or for the first time dont set errors
if (!result && (value == null || ((value is int || value is long) && (int)value == 0) || (value is decimal && (decimal)value == 0)))
return null;
if (!result)
{
ValidationResult validationResult = results.First();
error = validationResult.ErrorMessage;
}
return error;
}
#endregion Protected
#region PropertyChanged
[field: NonSerialized]
public event PropertyChangedEventHandler PropertyChanged;
protected void NotifyPropertyChanged(string propertyName)
{
PropertyChangedEventHandler handler = PropertyChanged;
if (handler == null)
return;
var e = new PropertyChangedEventArgs(propertyName);
handler(this, e);
}
protected void NotifyPropertyChanged<T>(Expression<Func<T>> propertySelector)
{
PropertyChangedEventHandler propertyChanged = PropertyChanged;
if (propertyChanged == null)
return;
string propertyName = GetPropertyName(propertySelector);
propertyChanged(this, new PropertyChangedEventArgs(propertyName));
}
#endregion PropertyChanged
#region Data Validation
string IDataErrorInfo.Error
{
get
{
throw new NotSupportedException("IDataErrorInfo.Error is not supported, use IDataErrorInfo.this[propertyName] instead.");
}
}
string IDataErrorInfo.this[string propertyName]
{
get { return OnValidate(propertyName); }
}
#endregion Data Validation
#region Privates
private static string GetPropertyName(LambdaExpression expression)
{
var memberExpression = expression.Body as MemberExpression;
if (memberExpression == null)
{
throw new InvalidOperationException();
}
return memberExpression.Member.Name;
}
private object GetValue(string propertyName)
{
object value = null;
if (!_values.TryGetValue(propertyName, out value))
{
PropertyDescriptor propertyDescriptor = TypeDescriptor.GetProperties(GetType()).Find(propertyName, false);
if (propertyDescriptor == null)
throw new ArgumentNullException("propertyName","invalid property");
value = propertyDescriptor.GetValue(this);
if (value != null)
_values.Add(propertyName, value);
}
return value;
}
#endregion Privates
#region Icommand Test
public bool IsValid
{
get
{
if (_values == null)
return true;
//To validate each property which is in _values dictionary
return _values
.Select(property => OnValidate(property.Key))
.All(errorMessages => errorMessages != null && errorMessages.Length <= 0);
}
}
#endregion Icommand Test
}
Order Entity
public class OrderEntity:EntityBase
{
[Required(ErrorMessage="Name is Required")]
public string Name
{
get { return GetValue(() => Name); }
set { SetValue(() => Name, value); }
}
[Required(ErrorMessage = "OrderNumber is Required")]
public string OrderNumber
{
get { return GetValue(() => OrderNumber); }
set { SetValue(() => OrderNumber, value); }
}
[Required(ErrorMessage = "Quantity is Required")]
[Range(20,75,ErrorMessage="Quantity must be between 20 and 75")]
public int Quantity
{
get { return GetValue(() => Quantity); }
set { SetValue(() => Quantity, value); }
}
public short Status { get; set; }
}
ViewModel:
public class ViewModel : INotifyPropertyChanged
{
public ViewModel()
{
Order = new OrderEntity { Name = "someOrder",OrderNumber="my order", Quantity = 23 };
Order.IsValidChanged += Order_IsValidChanged;
}
void Order_IsValidChanged()
{
if (SaveCommand != null)//RaiseCanExecuteChanged so that Save button disable on error
SaveCommand.RaiseCanExecuteChanged();
}
OrderEntity order;
public OrderEntity Order
{
get { return order; }
set { order = value; OnPropertychanged("Order"); }
}
MyCommand saveCommand;
public MyCommand SaveCommand
{
get { return saveCommand ?? (saveCommand = new MyCommand(OnSave, () => Order != null && Order.IsValid)); }
}
void OnSave(object obj)
{
//Do save stuff here
}
public event PropertyChangedEventHandler PropertyChanged;
void OnPropertychanged(string propName)
{
if (PropertyChanged != null)
PropertyChanged(this, new PropertyChangedEventArgs(propName));
}
}
xaml.cs
public partial class MainWindow : Window
{
public MainWindow()
{
InitializeComponent();
DataContext = new ViewModel();
}
}
xaml
<StackPanel>
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="auto"></RowDefinition>
<RowDefinition Height="4"></RowDefinition>
<RowDefinition Height="auto"></RowDefinition>
<RowDefinition Height="4"></RowDefinition>
<RowDefinition Height="auto"></RowDefinition>
<RowDefinition Height="4"></RowDefinition>
</Grid.RowDefinitions>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="auto"/>
<ColumnDefinition Width="4"/>
<ColumnDefinition Width="*"/>
</Grid.ColumnDefinitions>
<TextBlock Text="Order Name" Grid.Row="0" Grid.Column="0"/>
<TextBox Text="{Binding Order.Name, ValidatesOnDataErrors=True}" Grid.Row="0" Grid.Column="2"/>
<TextBlock Text="Order Number" Grid.Row="2" Grid.Column="0"/>
<TextBox Text="{Binding Order.OrderNumber, ValidatesOnDataErrors=True}" Grid.Row="2" Grid.Column="2"/>
<TextBlock Text="Quantity" Grid.Row="4" Grid.Column="0"/>
<TextBox Text="{Binding Order.Quantity, ValidatesOnDataErrors=True}" Grid.Row="4" Grid.Column="2"/>
</Grid>
<Button Command="{Binding SaveCommand}" Content="Save"/>
</StackPanel>
You can try and test this code if it fit your needs. Currently it works for PropertyChange however we can make some changes can make it to work for bot PropertyChange or on some button click . Its 3:00 am already so i gotta sleep.
Update Validating from ViewModel using ValidationExtension
public static class ValidationExtension
{
public static void ValidateObject<T>(this T obj) where T : INotifyErrorObject
{
if (obj == null)
throw new ArgumentNullException("object to validate cannot be null");
obj.ClearErrors();//clear all errors
foreach (var item in GetProperties(obj))
{
obj.SetError(item.Name, string.Join(";", ValidateProperty(obj,item).ToArray())); //Set or remove error
}
}
public static void ValidateProperty<T>(this T obj,string propName) where T : INotifyErrorObject
{
if (obj == null || string.IsNullOrEmpty(propName))
throw new ArgumentNullException("object to validate cannot be null");
var propertyInfo = GetProperty(propName, obj);
if (propertyInfo != null)
{
obj.SetError(propertyInfo.Name, string.Join(";", ValidateProperty(obj,propertyInfo).ToArray())); //Set or remove error
}
}
public static IEnumerable<string> ValidateProperty<T>(this T obj,PropertyInfo propInfo)
{
if (obj == null || propInfo == null)
throw new ArgumentNullException("object to validate cannot be null");
var results = new List<ValidationResult>();
if (!Validator.TryValidateProperty(propInfo.GetValue(obj), new ValidationContext(obj, null, null) { MemberName = propInfo.Name }, results))
return results.Select(s => s.ErrorMessage);
return Enumerable.Empty<string>();
}
static IEnumerable<PropertyInfo> GetProperties(object obj)
{
return obj.GetType().GetProperties().Where(p => p.GetCustomAttributes(typeof(ValidationAttribute), true).Length > 0).Select(p => p);
}
static PropertyInfo GetProperty(string propName, object obj)
{
return obj.GetType().GetProperties().FirstOrDefault(p =>p.Name==propName && p.GetCustomAttributes(typeof(ValidationAttribute), true).Length > 0);
}
}
EntityBase
public interface INotifyErrorObject : INotifyPropertyChanged, IDataErrorInfo
{
void SetError(string propertyName, string error);
void ClearErrors();
}
public class EntityBaseBase : INotifyErrorObject
{
Dictionary<string, string> validationErrors;
public void SetError(string propName, string error)
{
string obj=null;
if (validationErrors.TryGetValue(propName, out obj))
{
if (string.IsNullOrEmpty(error)) //Remove error
validationErrors.Remove(propName);
else if (string.CompareOrdinal(error, obj) == 0) //if error is same as previous return
return;
else
validationErrors[propName] = error; //set error
}
else if (!string.IsNullOrEmpty(error))
validationErrors.Add(propName, error);
RaisePropertyChanged(propName);
}
public void ClearErrors()
{
var properties = validationErrors.Select(s => s.Value).ToList();
validationErrors.Clear();
//Raise property changed to reflect on UI
foreach (var item in properties)
{
RaisePropertyChanged(item);
}
}
public EntityBaseBase()
{
validationErrors = new Dictionary<string, string>();
}
public event PropertyChangedEventHandler PropertyChanged;
protected void RaisePropertyChanged(string propName)
{
if (PropertyChanged != null && !string.IsNullOrEmpty(propName))
PropertyChanged(this, new PropertyChangedEventArgs(propName));
}
public string Error
{
get { throw new NotImplementedException(); }
}
public string this[string columnName]
{
get
{
string obj=null;
if (validationErrors.TryGetValue(columnName, out obj))
return obj;
else
return null;
}
}
}
Entity
public class OrderEntity : EntityBaseBase
{
string name;
[Required(ErrorMessage = "Name is Required")]
public string Name
{
get { return name; }
set { name = value; RaisePropertyChanged("Name"); }
}
string orderNumber;
[Required(ErrorMessage = "OrderNumber is Required")]
public string OrderNumber
{
get { return orderNumber; }
set { orderNumber = value; RaisePropertyChanged("OrderNumber"); }
}
int quantity;
[Required(ErrorMessage = "Quantity is Required")]
[Range(20, 75, ErrorMessage = "Quantity must be between 20 and 75")]
public int Quantity
{
get { return quantity; }
set { quantity = value; RaisePropertyChanged("Quantity"); }
}
public short Status { get; set; }
}
ViewModel
public class ViewModel : INotifyPropertyChanged
{
public ViewModel()
{
Order = new OrderEntity { Name = "someOrder",OrderNumber="my order", Quantity = 23 };
}
OrderEntity order;
public OrderEntity Order
{
get { return order; }
set { order = value; OnPropertychanged("Order"); }
}
MyCommand saveCommand;
public MyCommand SaveCommand
{
get { return saveCommand ?? (saveCommand = new MyCommand(OnSave, () => Order != null)); }
}
//ValidateObject on Some button Command
void OnSave(object obj)
{
Order.ValidateObject();
}
public event PropertyChangedEventHandler PropertyChanged;
void OnPropertychanged(string propName)
{
if (PropertyChanged != null)
PropertyChanged(this, new PropertyChangedEventArgs(propName));
}
}
xaml and xaml.cs is same as above. Order.ValidateObject validates the object on SaveCommand. Now if you want to Validate on PropertyChange from ViewModel then your ViewModel will have to Listen PropertyChanged event of Order and will have to call ValidateProperty of ValidationExtension like
public ViewModel()
{
Order = new OrderEntity { Name = "someOrder",OrderNumber="my order", Quantity = 23 };
Order.PropertyChanged += (o, args) => ((INotifyErrorObject)o).ValidateProperty(args.PropertyName);
}
I hope this will help.
Related
I make a Notes application using WPF with MVVM.
I want to make a counter for each i
So there is a task list, every task has importance (none, regular and important).
I want to make listbox, that displays a count of tasks of each importances and bind it to view (counter for each importance), but i don`t know how. Something like that -
total - 10
none - 5
regular - 3
important - 2
Task Model:
public enum TaskStates
{
None,
Regular,
Important,
}
[DataContractAttribute]
public class Task : INotifyPropertyChanged
{
private string _name;
private string _desc;
private TaskStates taskState;
public SolidColorBrush TaskBG { get; set; }
[DataMember]
public DateTime CreationDate { get; private set; }
[DataMember]
public TaskStates TaskState
{
get { return taskState; }
set
{
TaskBG ??= new SolidColorBrush();
switch (value)
{
case TaskStates.None:
TaskBG.Color = Color.FromRgb(0, 113, 127);
break;
case TaskStates.Important:
TaskBG.Color = Color.FromRgb(180, 60, 60);
break;
case TaskStates.Regular:
TaskBG.Color = Color.FromRgb(53, 165, 75);
break;
}
taskState = value;
OnPropertyChanged(nameof(TaskState));
}
}
[DataMember]
public string Name
{
get => _name;
set
{
_name = value;
OnPropertyChanged(nameof(Name));
}
}
[DataMember]
public string Description
{
get => _desc;
set
{
_desc = value;
OnPropertyChanged(nameof(Description));
}
}
public Task(string name, string desc,DateTime creationDate, TaskStates taskState)
{
this._name = name;
this._desc = desc;
CreationDate = creationDate;
TaskState = taskState;
}
public Task()
{
}
protected void OnPropertyChanged ([CallerMemberName]string prop = "") => PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(prop));
public event PropertyChangedEventHandler PropertyChanged;
}
ViewModel:
class TaskViewModel : INotifyPropertyChanged
{
public Task CurrentTask { get; set; }
public DateTime CurrentDate { get; set; }
IDialogService dialogService;
IFileService fileService;
private TaskCommand addTask;
private TaskCommand removeTask;
private TaskCommand saveCommand;
private TaskCommand openCommand;
private TaskCommand clearCommand;
public ObservableCollection<Task> Tasks { get; set; }
#region Command Properties
public TaskCommand AddCommand
{
get
{
return addTask ?? (addTask = new TaskCommand(
new Action<object>(obj =>
{
Tasks.Add((obj as Task) ?? new Task("Header", "smth", CurrentDate, TaskStates.None));
})
));
}
}
public TaskCommand RemoveCommand
{
get
{
return removeTask ?? (removeTask = new TaskCommand(
new Action<object>(obj =>
{
if (CurrentTask != null)
Tasks.Remove(CurrentTask);
}), new Func<object, bool>(obj => Tasks.Count > 0)
));
}
}
public TaskCommand SaveCommand
{
get
{
return saveCommand ?? (saveCommand = new TaskCommand(
new Action<object>(obj =>
{
try
{
if (dialogService.SaveFileDialog() == true)
{
fileService.Save(Tasks.ToList(), dialogService.FilePath);
dialogService.ShowMessage("File Saved");
}
}
catch (Exception ex)
{
dialogService.ShowMessage(ex.Message);
}
}),
new Func<object, bool>(obj => Tasks.Count > 0)));
}
}
public TaskCommand OpenCommand
{
get
{
return openCommand ?? (openCommand = new TaskCommand(
new Action<object>(obj =>
{
try
{
if (dialogService.OpenFileDialog())
{
var newTasks = fileService.Open(dialogService.FilePath);
if (newTasks.Count > 0)
{
Tasks.Clear();
foreach (var item in newTasks)
{
Tasks.Add(item);
}
}
}
}
catch (Exception ex)
{
dialogService.ShowMessage(ex.Message);
}
}
)));
}
}
public TaskCommand ClearCommand
{
get
{
return clearCommand ?? (clearCommand = new TaskCommand
(
new Action<object>(obj =>
{
var res = MessageBox.Show("Are you Sure?", "Caution", MessageBoxButton.YesNo, MessageBoxImage.Warning);
if (res == MessageBoxResult.Yes)
Tasks.Clear();
}),
new Func<object, bool>(obj => Tasks.Count > 0)
));
}
}
#endregion
public TaskViewModel()
{
CurrentDate = DateTime.Now;
fileService = new JsonFileService();
dialogService = new DefaultDialogService();
Tasks = new ObservableCollection<Task>()
{
new Task("Important", "Test",CurrentDate,TaskStates.Important),
new Task("Regular", "Test",CurrentDate,TaskStates.None),
};
}
protected void OnPropertyChanged([CallerMemberName] string prop = "") => PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(prop));
public event PropertyChangedEventHandler PropertyChanged;
}
Use a ValueConverter to accomplish this.
public class TaskStateCountConverter : IValueConverter
{
public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
=> $"{parameter} - {((IEnumerable<Task>)value).Count(task => task.TaskState == (TaskStates)parameter)}";
public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
=> throw new NotImplementedException();
}
XAML (you said you wanted a ListBox):
<Window.Resources>
<local:TaskStateCountConverter x:Key="TaskStateCountConverter"/>
</Window.Resources>
<Grid>
<ListBox>
<ListBoxItem Content="{Binding Tasks.Count}"/>
<ListBoxItem Content="{Binding Tasks,
Converter={StaticResource TaskStateCountConverter},
ConverterParameter={x:Static local:TaskStates.None}}"/>
<ListBoxItem Content="{Binding Tasks,
Converter={StaticResource TaskStateCountConverter},
ConverterParameter={x:Static local:TaskStates.Regular}}"/>
<ListBoxItem Content="{Binding Tasks,
Converter={StaticResource TaskStateCountConverter},
ConverterParameter={x:Static local:TaskStates.Important}}"/>
</ListBox>
</Grid>
ViewModel:
public class TaskViewModel : INotifyPropertyChanged
{
// unchanged parts skipped
public TaskViewModel()
{
Tasks.CollectionChanged += OnTasksChanged;
}
private void OnTasksChanged(object sender, EventArgs e)
=> OnPropertyChanged(nameof(Tasks));
It was a bit trickier than I first thought because the converted values are not updated without the event handler (Tasks.Count is).
And while you're about it, you could also create a ValueConverter for your coloring logic.
Edit
To update TaskViewModel from within CurrentTask:
public class TaskViewModel : INotifyPropertyChanged
{
// unchanged parts skipped
private Task _currentTask;
public Task CurrentTask
{
get => _currentTask;
set
{
if (value != _currentTask)
{
if (_currentTask != null)
{
_currentTask.PropertyChanged -= OnCurrentTaskChanged;
}
_currentTask = value;
_currentTask.PropertyChanged += OnCurrentTaskChanged;
}
}
}
private void OnCurrentTaskChanged(object sender, PropertyChangedEventArgs e)
{
if (e.PropertyName == nameof(Task.TaskState))
{
OnPropertyChanged(nameof(Tasks));
}
}
}
My Model :
public class Cont: INotifyPropertyChanged
{
private string _Contact;
public string Contact
{
get { return _Contact; }
set
{
_Contact= value;
if (PropertyChanged != null)
PropertyChanged(this, new PropertyChangedEventArgs("Contact"));
}
}
private string _Age;
public string Age
{
get { return _Age; }
set
{
_Age = value;
if (PropertyChanged != null)
PropertyChanged(this, new PropertyChangedEventArgs("Age"));
}
}
My CollectionViewSource:
<CollectionViewSource x:Key="group" Source="{Binding GroupedCollection}" ItemsPath="{Binding GroupedCollection[0].ContactColl}" IsSourceGrouped="True">
</CollectionViewSource>
My GridView :
<GridView ItemsSource="{Binding Source={StaticResource group}}"
SelectionMode="None" ScrollViewer.VerticalScrollBarVisibility="Hidden">
<GridView.GroupStyle>
<GroupStyle HidesIfEmpty="True">
<GroupStyle.HeaderTemplate>
<DataTemplate>
<TextBlock x:Name="keyHeader" Text="{Binding Key}"/>
</DataTemplate>
</GroupStyle.HeaderTemplate>
</GroupStyle>
</GridView.GroupStyle>
<GridView.ItemTemplate>
<DataTemplate>
<TextBlock Text="{Binding Contact}"/>
</DataTemplate>
</GridView.ItemTemplate>
</GridView>
My ViewModel :
public class ContVM: INotifyPropertyChanged
{
public ObservableCollection<Cont> Contlist = new ObservableCollection<Cont>();
private ObservableCollection<ContItem> _GroupedCollection;
public ObservableCollection<ContItem> GroupedCollection
{
get
{
if (_GroupedCollection == null)
{
_GroupedCollection= new ObservableCollection<ContItem>();
}
return _GroupedCollection;
}
set
{
_GroupedCollection = value;
if (PropertyChanged != null)
PropertyChanged(this, new PropertyChangedEventArgs("GroupedCollection"));
}
}
public void SetInitialCollection()
{
var keyList = Contlist .GroupBy(c => c.Age).Select(g => g.Key);
foreach (var key in keyList)
{
var contItem= new ContItem();
contItem.Key = key;
var contList = Contlist.Where(c => c.Age == key);
foreach (var item in contList )
{
contItem.ContactColl.Add(item);
}
GroupedCollection.Add(contItem);
}
}
public void AddNew(Cont Item)
{
var contItem = GroupedCollection.FirstOrDefault(g => g.Key == Item.Age );
if ( contItem != null )
{
contItem.ContactColl.Add(Item);
}
else
{
contItem = new ContItem();
contItem.Key = Item.Age ;
GroupedCollection.Add(contItem);
}
}
public void Delete(Cont Item)
{
var contItem = GroupedCollection.FirstOrDefault(g => g.Key == Item.Age);
if (contItem != null)
{
if (contItem.ContactColl.Contains(Item))
{
contItem.ContactColl.Remove(Item);
}
}
}
public event PropertyChangedEventHandler PropertyChanged;
}
public class ContItem : INotifyPropertyChanged
{
public string Key { get; set; }
private ObservableCollection<Cont> _ContactColl;
public ObservableCollection<Cont> ContactColl
{
get
{
if (_ContactColl== null)
{
_ContactColl= new ObservableCollection<Cont>();
}
return _ContactColl;
}
set
{
_ContactColl= value;
if (PropertyChanged != null)
PropertyChanged(this, new PropertyChangedEventArgs("ContactColl"));
}
}
public event PropertyChangedEventHandler PropertyChanged;
}
My code-behind cs :
ContVM vm = new ContVM();
private void Add_new_cont_Click(object sender, RoutedEventArgs e)
{
// a button that add a new data from 2 textbox
Cont item = new Cont();
item.Contact = textbox1.Text;
item.Contact = textbox2.Text;
vm.AddNew(item); // error line
}
Already tried this, but I get error 0xC0000005: Access violation reading location 0x00000004. and it redirect me at line which is vm.AddNew(item);. All these codes have been updated/guided by #Bells.
Each time when a new item is being added to the collection, you are regrouping the items and resetting the source. That will cause the grid view to reset its position. In order to maintain the position, you have to assign a source collection initially as the ItemsSource and manipulations should be done on the same collection.
public class Cont
{
public string Contact { get; set; }
public string Age { get; set; }
}
public class ContItem
{
public string Key { get; set; }
private ObservableCollection<Cont> _contactColl;
public ObservableCollection<Cont> ContactColl
{
get
{
if (_contactColl == null)
_contactColl = new ObservableCollection<Cont>();
return _contactColl;
}
}
}
public class TestClass
{
public ObservableCollection<Cont> Contlist = new ObservableCollection<Cont>();
private ObservableCollection<ContItem> _groupedCollection;
public ObservableCollection<ContItem> GroupedCollection
{
get
{
if (_groupedCollection == null)
_groupedCollection = new ObservableCollection<ContItem>();
return _groupedCollection;
}
}
public void SetInitialCollection()
{
//Add the existing items in ContactList (if any) to a grouped collection.
var keyList = Contlist.GroupBy(c => c.Age).Select(g => g.Key);
foreach (var key in keyList)
{
var contItem = new ContItem();
contItem.Key = key;
var contList = Contlist.Where(c => c.Age == key);
foreach (var item in contList)
{
contItem.ContactColl.Add(item);
}
GroupedCollection.Add(contItem);
}
}
public void AddNewItem()
{
var cont = new Cont();
cont.Age = "32";
cont.Contact = "";
var contItem = GroupedCollection.FirstOrDefault(g => g.Key == cont.Age);
if (contItem != null)
{
contItem.ContactColl.Add(cont);
}
else
{
contItem = new ContItem();
contItem.Key = cont.Age;
contItem.ContactColl.Add(cont);
GroupedCollection.Add(contItem);
}
}
public void DeleteItem(Cont cont)
{
var contItem = GroupedCollection.FirstOrDefault(g => g.Key == cont.Age);
if (contItem != null)
{
if (contItem.ContactColl.Contains(cont))
{
contItem.ContactColl.Remove(cont);
}
}
}
}
<CollectionViewSource x:Key="group" Source="{Binding GroupedCollection}"
ItemsPath="ContactColl" IsSourceGrouped="True" />
I've faced a really strange behavior while using CollectionViews and wonder if somebody can explain me that behavior.
I've got an ObservableCollection, lets call it _sourcelist. Then I create several CollectionViews of _sourcelist, in that way:
CollectionViewSource cvs = new CollectionViewSource() { Source = _sourcelist };
ICollectionView list = cvs.View;
These CollectionViews are each used with a different filter and one CollectionView with no filter (so representing the original source). In my Application I have a TabControl where every TabItem contains a ListBox with one of these CollectionViews as ItemsSource. When I know delete an item from the sourcelist, the ListBox with the CollectionView with no filter gets updated, but the other ListBoxes that contains filtered CollectionViews and contain the deleted item don't get updated. So the deleted item remains in that ListBoxes.
Now, the strange behavior is following:
When I add the following code after code written above:
list.CollectionChanged += (o,e) => { };
Then the other ListBoxes get updated, too.
Why that? I mean, I just registered an event handler for the CollectionChanged event, which does nothing!
So, here is my Code Sample. I know it is a big amount of code, but it is ready to run and reproduces the strange behavior.
When you run the code and try to delete an item in the "All" List, then the corresponding item in the filtered "Under 30" or "Over 30" list will not be deleted. Now uncomment the commented line in the ListViewModel (it is just a registration of an empty Eventhandler), then the corresponding items in the filtered lists will be deleted.
If you have any questions, then please ask! :-)
Thank you for your interest in solving the problem. :-)
P.S.: The RelayCommand is from the assembly: Microsoft.TeamFoundation.Controls
MainWindow.xaml:
<Window x:Class="CollectionTest.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="clr-namespace:CollectionTest"
Title="MainWindow" Height="350" Width="525">
<Window.Resources>
<DataTemplate DataType="{x:Type local:ListViewModel}">
<ListBox ItemsSource="{Binding List}" SelectedItem="{Binding Selected}" BorderBrush="Transparent" Margin="0" HorizontalContentAlignment="Stretch"/>
</DataTemplate>
<DataTemplate DataType="{x:Type local:PersonViewModel}">
<TextBlock Margin="5,0" VerticalAlignment="Center" Text="{Binding Name}"/>
</DataTemplate>
<Style x:Key="{x:Type TabItem}" TargetType="{x:Type TabItem}">
<Setter Property="Header" Value="{Binding Name}"/>
</Style>
</Window.Resources>
<DockPanel>
<Button DockPanel.Dock="Bottom" Name="DeleteButton" Content="Delete" Command="{Binding DeleteCommand}"/>
<TabControl Name="TabControl" ItemsSource="{Binding Lists}" Padding="0" SelectedItem="{Binding Selected}"/>
</DockPanel>
</Window>
MainWindow.xaml.cs:
public partial class MainWindow : Window
{
private MainViewModel mvm;
public MainWindow()
{
InitializeComponent();
mvm = new MainViewModel(CreateRepo());
this.DataContext = mvm;
}
private Repository CreateRepo()
{
Repository repo = new Repository();
for (int i = 20; i < 45; i++)
{
Person p = new Person() { Name = "Person" + i, Age = i };
repo.Add(p);
}
return repo;
}
}
Person:
public class Person
{
public string Name { get; set; }
public int Age { get; set; }
}
Repository:
public class Repository
{
private readonly List<Person> _list;
public event EventHandler<RepositoryChangedEventArgs> Added;
public event EventHandler<RepositoryChangedEventArgs> Removed;
public Repository()
{
this._list = new List<Person>();
}
public void Add(Person person)
{
if (person == null)
{
throw new ArgumentNullException("person");
}
if (!this._list.Contains(person))
{
this._list.Add(person);
if (this.Added != null)
{
this.Added(this, new RepositoryChangedEventArgs(person));
}
}
}
public void Remove(Person person)
{
if (person == null)
{
throw new ArgumentNullException("person");
}
if (this._list.Contains(person))
{
this._list.Remove(person);
if (this.Removed != null)
{
this.Removed(this, new RepositoryChangedEventArgs(person));
}
}
}
public List<Person> Get()
{
return new List<Person>(this._list);
}
}
public class RepositoryChangedEventArgs : EventArgs
{
public Person Entity { get; set; }
public RepositoryChangedEventArgs(Person entity)
{
this.Entity = entity;
}
}
PersonViewModel:
public class PersonViewModel : INotifyPropertyChanged
{
private Person _person;
private Repository _repository;
public string Name
{
get
{
return this._person.Name;
}
set
{
if (this._person.Name != value)
{
this._person.Name = value;
this.OnPropertyChanged("Name");
}
}
}
public int Age
{
get
{
return this._person.Age;
}
set
{
if (this._person.Age != value)
{
this._person.Age = value;
this.OnPropertyChanged("Age");
}
}
}
public PersonViewModel(Person person, Repository repository)
{
this._person = person;
this._repository = repository;
}
public void Delete()
{
this._repository.Remove(this._person);
}
public event PropertyChangedEventHandler PropertyChanged;
public void OnPropertyChanged(string name)
{
if (this.PropertyChanged != null)
{
PropertyChanged(this, new PropertyChangedEventArgs(name));
}
}
}
ListViewModel:
public class ListViewModel : INotifyPropertyChanged
{
private IEnumerable _sourceList;
private ICollectionView _list;
private PersonViewModel _selected;
private string _name;
public ICollectionView List
{
get
{
return this._list;
}
}
public PersonViewModel Selected
{
get
{
return this._selected;
}
set
{
if (this._selected != value)
{
this._selected = value;
this.OnPropertyChanged("Selected");
}
}
}
public string Name
{
get
{
return this._name;
}
set
{
if (this._name != value)
{
this._name = value;
this.OnPropertyChanged("Name");
}
}
}
public ListViewModel(string name, IEnumerable list)
{
this.Name = name;
this._sourceList = list;
CollectionViewSource cvs = new CollectionViewSource() { Source = list };
this._list = cvs.View;
// cvs.View.CollectionChanged += (o, e) => { }; // uncomment this line to let it work
}
public ListViewModel(String name, IEnumerable list, Predicate<object> filter)
: this(name, list)
{
this._list.Filter = filter;
}
public event PropertyChangedEventHandler PropertyChanged;
public void OnPropertyChanged(string name)
{
if (PropertyChanged != null)
{
PropertyChanged(this, new PropertyChangedEventArgs(name));
}
}
}
MainViewModel:
public class MainViewModel : INotifyPropertyChanged
{
protected ObservableCollection<PersonViewModel> _sourceList;
protected ObservableCollection<ListViewModel> _lists;
protected Repository _repository;
protected Dictionary<Person, PersonViewModel> _dictionary;
private RelayCommand _deleteCommand;
private ListViewModel _selected;
public ListViewModel Selected
{
get
{
return this._selected;
}
set
{
if (this._selected != value)
{
this._selected = value;
this.OnPropertyChanged("Selected");
}
}
}
public RelayCommand DeleteCommand
{
get
{
if (this._deleteCommand == null)
{
this._deleteCommand = new RelayCommand(this.ExecutedDelete, this.CanExecuteDelete);
}
return this._deleteCommand;
}
}
public IEnumerable<ListViewModel> Lists
{
get
{
return this._lists;
}
}
public MainViewModel(Repository repository)
{
this._sourceList = new ObservableCollection<PersonViewModel>();
this._lists = new ObservableCollection<ListViewModel>();
this._dictionary = new Dictionary<Person, PersonViewModel>();
this._repository = repository;
this._repository.Added += this.OnAdded;
this._repository.Removed += this.OnRemoved;
this.CreateSourceList(repository);
this.CreateLists();
if (this._lists.Count > 0)
{
this.Selected = this._lists[0];
}
}
protected void CreateSourceList(Repository repository)
{
foreach (Person person in repository.Get())
{
PersonViewModel pvm = new PersonViewModel(person, repository);
this._dictionary.Add(person, pvm);
this._sourceList.Add(pvm);
}
}
protected void CreateLists()
{
this._lists.Add(new ListViewModel("All", this._sourceList));
this._lists.Add(new ListViewModel("Under 30", this._sourceList,
delegate(object o)
{
PersonViewModel pvm = o as PersonViewModel;
return (pvm != null && pvm.Age < 30);
}));
this._lists.Add(new ListViewModel("Over 30", this._sourceList,
delegate(object o)
{
PersonViewModel pvm = o as PersonViewModel;
return (pvm != null && pvm.Age > 30);
}));
}
protected void Remove(Person person)
{
PersonViewModel pvm = this._dictionary[person];
this._dictionary.Remove(person);
this._sourceList.Remove(pvm);
}
protected void OnAdded(object sender, RepositoryChangedEventArgs args)
{
Person addedPerson = args.Entity;
if (addedPerson != null)
{
PersonViewModel pvm = new PersonViewModel(addedPerson, this._repository);
this._dictionary.Add(addedPerson, pvm);
this._sourceList.Add(pvm);
}
}
protected void OnRemoved(object sender, RepositoryChangedEventArgs args)
{
Person removedPerson = args.Entity;
if (removedPerson != null)
{
this.Remove(removedPerson);
}
}
private bool CanExecuteDelete(object o)
{
return true;
}
private void ExecutedDelete(object o)
{
PersonViewModel pvm = null;
if (this.Selected != null && (pvm = this.Selected.Selected) != null)
{
pvm.Delete();
}
}
public event PropertyChangedEventHandler PropertyChanged;
public void OnPropertyChanged(string name)
{
if (PropertyChanged != null)
{
PropertyChanged(this, new PropertyChangedEventArgs(name));
}
}
}
What is the best way to create a class with an event that fires when one of its Properties is changed? Specifically, how do you convey to any subscribers which Property was changed?
Ex:
public class ValueChangedPublisher
{
private int _prop1;
private string _prop2;
public static event ValueChangedHandler(/*some parameters?*/);
public int Prop1
{
get { return _prop1; }
set
{
if (_prop1 != value)
{
_prop1 = value;
ValueChangedHandler(/*parameters?*/);
}
}
}
public string Prop2
{
get { return _prop2; }
set
{
if (_prop2 != value)
{
_prop2 = value;
ValueChangedHandler(/*parameters?*/);
}
}
}
}
public class ValueChangedSubscriber
{
private int _prop1;
private string _prop2;
public ValueChangedSubscriber()
{
ValueChangedPublisher.ValueChanged += ValueChanged;
}
private void ValueChanged(/*parameters?*/)
{
/*how does the subscriber know which property was changed?*/
}
}
My goal is to make this as extensible as possible (e.g. I don't want a bunch of huge if/else if/switch statements lumbering around). Does anybody know of a technique to achieve what I'm looking for?
EDIT:
What I'm really looking for is how to utilize the INotifyPropertyChanged pattern on the subscriber side. I don't want to do this:
private void ValueChanged(string propertyName)
{
switch(propertyName)
{
case "Prop1":
_prop1 = _valueChangedPublisher.Prop1;
break;
case "Prop2":
_prop2 = _valueChangedPublisher.Prop2;
break;
// the more properties that are added to the publisher, the more cases I
// have to handle here :/ I don't want to have to do it this way
}
}
.NET provides the INotifyPropertyChanged interface. Inherit and implement it:
public class ValueChangedPublisher : INotifyPropertyChanged
{
private int _prop1;
private string _prop2;
public event PropertyChangedEventHandler ValueChangedHandler;
private void NotifyPropertyChanged([CallerMemberName] String propertyName = "")
{
if (PropertyChanged != null)
{
PropertyChanged(this, new PropertyChangedEventArgs(propertyName));
}
}
public int Prop1
{
get { return _prop1; }
set
{
if (_prop1 != value)
{
_prop1 = value;
NotifyPropertyChanged();
}
}
}
public string Prop2
{
get { return _prop2; }
set
{
if (_prop2 != value)
{
_prop2 = value;
NotifyPropertyChanged();
}
}
}
}
Here is what I did to solve my problem. It's a bit large, so maybe not performant, but works for me.
Edit
See this question for performance details: C# using properties with value types with Delegate.CreateDelegate
Base Class for publishing property change stuff:
public abstract class PropertyChangePublisherBase : INotifyPropertyChanged
{
private Dictionary<string, PropertyInfo> _properties;
private bool _cacheProperties;
public bool CacheProperties
{
get { return _cacheProperties; }
set
{
_cacheProperties = value;
if (_cacheProperties && _properties == null)
_properties = new Dictionary<string, PropertyInfo>();
}
}
protected PropertyChangePublisherBase(bool cacheProperties)
{
CacheProperties = cacheProperties;
}
public bool ContainsBinding(PropertyChangedEventHandler handler)
{
if (PropertyChanged == null)
return false;
return PropertyChanged.GetInvocationList().Contains(handler);
}
public object GetPropertValue(string propertyName)
{
if (String.IsNullOrEmpty(propertyName) || String.IsNullOrWhiteSpace(propertyName))
throw new ArgumentException("Argument must be the name of a property of the current instance.", "propertyName");
return ProcessGetPropertyValue(propertyName);
}
protected virtual object ProcessGetPropertyValue(string propertyName)
{
if (_cacheProperties)
{
if (_properties.ContainsKey(propertyName))
{
return _properties[propertyName].GetValue(this, null);
}
else
{
var property = GetType().GetProperty(propertyName);
_properties.Add(propertyName, property);
return property.GetValue(this, null);
}
}
else
{
var property = GetType().GetProperty(propertyName);
return property.GetValue(this, null);
}
}
#region INotifyPropertyChanged Implementation
public event PropertyChangedEventHandler PropertyChanged;
[NotifyPropertyChangedInvocator]
protected virtual void RaisePropertyChanged(string propertyName)
{
PropertyChangedEventHandler handler = PropertyChanged;
if (handler != null)
handler(this, new PropertyChangedEventArgs(propertyName));
}
#endregion
}
Base Class for receiving property change stuff:
public abstract class PropertyChangeSubscriberBase
{
protected readonly string _propertyName;
protected virtual object Value { get; set; }
protected PropertyChangeSubscriberBase(string propertyName, PropertyChangePublisherBase bindingPublisher)
{
_propertyName = propertyName;
AddBinding(propertyName, this, bindingPublisher);
}
~PropertyChangeSubscriberBase()
{
RemoveBinding(_propertyName);
}
public void Unbind()
{
RemoveBinding(_propertyName);
}
#region Static Fields
private static List<string> _bindingNames = new List<string>();
private static List<PropertyChangeSubscriberBase> _subscribers = new List<PropertyChangeSubscriberBase>();
private static List<PropertyChangePublisherBase> _publishers = new List<PropertyChangePublisherBase>();
#endregion
#region Static Methods
private static void PropertyChanged(object sender, PropertyChangedEventArgs args)
{
string propertyName = args.PropertyName;
if (_bindingNames.Contains(propertyName))
{
int i = _bindingNames.IndexOf(propertyName);
var publisher = _publishers[i];
var subscriber = _subscribers[i];
subscriber.Value = publisher.GetPropertValue(propertyName);
}
}
public static void AddBinding(string propertyName, PropertyChangeSubscriberBase subscriber, PropertyChangePublisherBase publisher)
{
if (!_bindingNames.Contains(propertyName))
{
_bindingNames.Add(propertyName);
_publishers.Add(publisher);
_subscribers.Add(subscriber);
if (!publisher.ContainsBinding(PropertyChanged))
publisher.PropertyChanged += PropertyChanged;
}
}
public static void RemoveBinding(string propertyName)
{
if (_bindingNames.Contains(propertyName))
{
int i = _bindingNames.IndexOf(propertyName);
var publisher = _publishers[i];
_bindingNames.RemoveAt(i);
_publishers.RemoveAt(i);
_subscribers.RemoveAt(i);
if (!_publishers.Contains(publisher))
publisher.PropertyChanged -= PropertyChanged;
}
}
#endregion
}
Actual class to use for subscribing to property change stuff:
public sealed class PropertyChangeSubscriber<T> : PropertyChangeSubscriberBase
{
private PropertyChangePublisherBase _publisher;
public new T Value
{
get
{
if (base.Value == null)
return default(T);
if (base.Value.GetType() != typeof(T))
throw new InvalidOperationException(String.Format("Property {0} on object of type {1} does not match the type Generic type specified {2}.", _propertyName, _publisher.GetType(), typeof(T)));
return (T)base.Value;
}
set { base.Value = value; }
}
public PropertyChangeSubscriber(string propertyName, PropertyChangePublisherBase bindingPublisher)
: base(propertyName, bindingPublisher)
{
_publisher = bindingPublisher;
}
}
Here is an example of the Class with properties that you wish to be notified about:
public class ExamplePublisher: PropertyChangedPublisherBase
{
private string _id;
private bool _testBool;
public string Id
{
get { return _id; }
set
{
if (value == _id) return;
_id = value;
RaisePropertyChanged("Id");
}
}
public bool TestBool
{
get { return _testBool; }
set
{
if (value.Equals(_testBool)) return;
_testBool = value;
RaisePropertyChanged("TestBool");
}
}
}
Here is an example of the Class that will be notified when the properties in the class above change:
public class ExampleReceiver
{
public PropertyChangeSubscriber<string> Id { get; set; }
public PropertyChangeSubscriber<bool> TestBool { get; set; }
public MyExampleClass(PropertyChangePublisherBase publisher)
{
Id = new PropertyChangeSubscriber<string>("Id", publisher);
TestBool = new PropertyChangeSubscriber<bool>("TestBool", publisher);
}
}
I am implementing the observer pattern for our application - currently playing around with the RX Framework.
I currently have an example that looks like this:
Observable.FromEventPattern<PropertyChangedEventArgs>(Instance.Address, "PropertyChanged")
.Where(e => e.EventArgs.PropertyName == "City")
.ObserveOn(Scheduler.ThreadPool)
.Subscribe(search => OnNewSearch(search.EventArgs));
(I have a similar one for "PropertyChanging")
The EventArgs don't give me much. What I would like is an extension of the EventArgs that would give me the ability to see the previous and new values, as well as the ability to mark the event in the 'changing' listener, such that the change wouldn't actually persist. How can this be done? Thanks.
I think that it comes down to how you implement the INotifyPropertyChanging and INotifyPropertyChanged interfaces.
The PropertyChangingEventArgs and PropertyChangedEventArgs classes unfortunately don't provide a before and after value of the property or the ability to cancel the change, but you can derive your own event args classes that do provide that functionality.
First, define the following event args classes. Notice that these derive from the PropertyChangingEventArgs class and PropertyChangedEventArgs class. This allows us to pass these objects as arguments to the PropertyChangingEventHandler and PropertyChangedEventHandler delegates.
class PropertyChangingCancelEventArgs : PropertyChangingEventArgs
{
public bool Cancel { get; set; }
public PropertyChangingCancelEventArgs(string propertyName)
: base(propertyName)
{
}
}
class PropertyChangingCancelEventArgs<T> : PropertyChangingCancelEventArgs
{
public T OriginalValue { get; private set; }
public T NewValue { get; private set; }
public PropertyChangingCancelEventArgs(string propertyName, T originalValue, T newValue)
: base(propertyName)
{
this.OriginalValue = originalValue;
this.NewValue = newValue;
}
}
class PropertyChangedEventArgs<T> : PropertyChangedEventArgs
{
public T PreviousValue { get; private set; }
public T CurrentValue { get; private set; }
public PropertyChangedEventArgs(string propertyName, T previousValue, T currentValue)
: base(propertyName)
{
this.PreviousValue = previousValue;
this.CurrentValue = currentValue;
}
}
Next, you would need to use these classes in your implementation of the INotifyPropertyChanging and INotifyPropertyChanged interfaces. An example of an implementation is the following:
class Example : INotifyPropertyChanging, INotifyPropertyChanged
{
public event PropertyChangingEventHandler PropertyChanging;
public event PropertyChangedEventHandler PropertyChanged;
protected bool OnPropertyChanging<T>(string propertyName, T originalValue, T newValue)
{
var handler = this.PropertyChanging;
if (handler != null)
{
var args = new PropertyChangingCancelEventArgs<T>(propertyName, originalValue, newValue);
handler(this, args);
return !args.Cancel;
}
return true;
}
protected void OnPropertyChanged<T>(string propertyName, T previousValue, T currentValue)
{
var handler = this.PropertyChanged;
if (handler != null)
handler(this, new PropertyChangedEventArgs<T>(propertyName, previousValue, currentValue));
}
int _ExampleValue;
public int ExampleValue
{
get { return _ExampleValue; }
set
{
if (_ExampleValue != value)
{
if (this.OnPropertyChanging("ExampleValue", _ExampleValue, value))
{
var previousValue = _ExampleValue;
_ExampleValue = value;
this.OnPropertyChanged("ExampleValue", previousValue, value);
}
}
}
}
}
Note, your event handlers for the PropertyChanging and PropertyChanged events will still need to take the original PropertyChangingEventArgs class and PropertyChangedEventArgs class as parameters, rather than a more specific version. However, you will be able to cast the event args objects to your more specific types in order to access the new properties.
Below is an example of event handlers for these events:
class Program
{
static void Main(string[] args)
{
var exampleObject = new Example();
exampleObject.PropertyChanging += new PropertyChangingEventHandler(exampleObject_PropertyChanging);
exampleObject.PropertyChanged += new PropertyChangedEventHandler(exampleObject_PropertyChanged);
exampleObject.ExampleValue = 123;
exampleObject.ExampleValue = 100;
}
static void exampleObject_PropertyChanging(object sender, PropertyChangingEventArgs e)
{
if (e.PropertyName == "ExampleValue")
{
int originalValue = ((PropertyChangingCancelEventArgs<int>)e).OriginalValue;
int newValue = ((PropertyChangingCancelEventArgs<int>)e).NewValue;
// do not allow the property to be changed if the new value is less than the original value
if(newValue < originalValue)
((PropertyChangingCancelEventArgs)e).Cancel = true;
}
}
static void exampleObject_PropertyChanged(object sender, PropertyChangedEventArgs e)
{
if (e.PropertyName == "ExampleValue")
{
int previousValue = ((PropertyChangedEventArgs<int>)e).PreviousValue;
int currentValue = ((PropertyChangedEventArgs<int>)e).CurrentValue;
}
}
}
The accepted response is really bad, you can do that simply with Buffer().
Observable.FromEventPattern<PropertyChangedEventArgs>(Instance.Address, "PropertyChanged")
.Where(e => e.EventArgs.PropertyName == "City")
.Buffer(2,1) //Take 2 events at a time, every 1 event
.ObserveOn(Scheduler.ThreadPool)
.Subscribe(search => ...); //search[0] is old value, search[1] is new value
For anyone that does want the best of both RX and being able to Cancel here is a hybrid of both of these ideas
The ViewModel base class stuff
public abstract class INPCBase : INotifyPropertyChanged, INotifyPropertyChanging
{
public event PropertyChangingEventHandler PropertyChanging;
public event PropertyChangedEventHandler PropertyChanged;
protected bool OnPropertyChanging<T>(string propertyName, T originalValue, T newValue)
{
var handler = this.PropertyChanging;
if (handler != null)
{
var args = new PropertyChangingCancelEventArgs<T>(propertyName, originalValue, newValue);
handler(this, args);
return !args.Cancel;
}
return true;
}
protected void OnPropertyChanged<T>(string propertyName, T previousValue, T currentValue)
{
var handler = this.PropertyChanged;
if (handler != null)
handler(this, new PropertyChangedEventArgs<T>(propertyName, previousValue, currentValue));
}
}
public class PropertyChangingCancelEventArgs : PropertyChangingEventArgs
{
public bool Cancel { get; set; }
public PropertyChangingCancelEventArgs(string propertyName)
: base(propertyName)
{
}
}
public class PropertyChangingCancelEventArgs<T> : PropertyChangingCancelEventArgs
{
public T OriginalValue { get; private set; }
public T NewValue { get; private set; }
public PropertyChangingCancelEventArgs(string propertyName, T originalValue, T newValue)
: base(propertyName)
{
this.OriginalValue = originalValue;
this.NewValue = newValue;
}
}
public class PropertyChangedEventArgs<T> : PropertyChangedEventArgs
{
public T PreviousValue { get; private set; }
public T CurrentValue { get; private set; }
public PropertyChangedEventArgs(string propertyName, T previousValue, T currentValue)
: base(propertyName)
{
this.PreviousValue = previousValue;
this.CurrentValue = currentValue;
}
}
Then I have these couple extensions.
One to get the property name from Expression tree
public static class ExpressionExtensions
{
public static string GetPropertyName<TProperty>(this Expression<Func<TProperty>> expression)
{
var memberExpression = expression.Body as MemberExpression;
if (memberExpression == null)
{
var unaryExpression = expression.Body as UnaryExpression;
if (unaryExpression != null)
{
if (unaryExpression.NodeType == ExpressionType.ArrayLength)
return "Length";
memberExpression = unaryExpression.Operand as MemberExpression;
if (memberExpression == null)
{
var methodCallExpression = unaryExpression.Operand as MethodCallExpression;
if (methodCallExpression == null)
throw new NotImplementedException();
var arg = (ConstantExpression)methodCallExpression.Arguments[2];
return ((MethodInfo)arg.Value).Name;
}
}
else
throw new NotImplementedException();
}
var propertyName = memberExpression.Member.Name;
return propertyName;
}
public static string GetPropertyName<T, TProperty>(this Expression<Func<T, TProperty>> expression)
{
var memberExpression = expression.Body as MemberExpression;
if (memberExpression == null)
{
var unaryExpression = expression.Body as UnaryExpression;
if (unaryExpression != null)
{
if (unaryExpression.NodeType == ExpressionType.ArrayLength)
return "Length";
memberExpression = unaryExpression.Operand as MemberExpression;
if (memberExpression == null)
{
var methodCallExpression = unaryExpression.Operand as MethodCallExpression;
if (methodCallExpression == null)
throw new NotImplementedException();
var arg = (ConstantExpression)methodCallExpression.Arguments[2];
return ((MethodInfo)arg.Value).Name;
}
}
else
throw new NotImplementedException();
}
var propertyName = memberExpression.Member.Name;
return propertyName;
}
public static String PropertyToString<R>(this Expression<Func<R>> action)
{
MemberExpression ex = (MemberExpression)action.Body;
return ex.Member.Name;
}
public static void CheckIsNotNull<R>(this Expression<Func<R>> action, string message)
{
MemberExpression ex = (MemberExpression)action.Body;
string memberName = ex.Member.Name;
if (action.Compile()() == null)
{
throw new ArgumentNullException(memberName, message);
}
}
}
And then the Rx part
public static class ObservableExtensions
{
public static IObservable<ItemPropertyChangingEvent<TItem, TProperty>> ObserveSpecificPropertyChanging<TItem, TProperty>(
this TItem target, Expression<Func<TItem, TProperty>> propertyName) where TItem : INotifyPropertyChanging
{
var property = propertyName.GetPropertyName();
return ObserveSpecificPropertyChanging(target, property)
.Select(i => new ItemPropertyChangingEvent<TItem, TProperty>()
{
OriginalEventArgs = (PropertyChangingCancelEventArgs<TProperty>)i.OriginalEventArgs,
Property = i.Property,
Sender = i.Sender
});
}
public static IObservable<ItemPropertyChangingEvent<TItem>> ObserveSpecificPropertyChanging<TItem>(
this TItem target, string propertyName = null) where TItem : INotifyPropertyChanging
{
return Observable.Create<ItemPropertyChangingEvent<TItem>>(obs =>
{
Dictionary<string, PropertyInfo> properties = new Dictionary<string, PropertyInfo>();
PropertyChangingEventHandler handler = null;
handler = (s, a) =>
{
if (propertyName == null || propertyName == a.PropertyName)
{
PropertyInfo prop;
if (!properties.TryGetValue(a.PropertyName, out prop))
{
prop = target.GetType().GetProperty(a.PropertyName);
properties.Add(a.PropertyName, prop);
}
var change = new ItemPropertyChangingEvent<TItem>()
{
Sender = target,
Property = prop,
OriginalEventArgs = a,
};
obs.OnNext(change);
}
};
target.PropertyChanging += handler;
return () =>
{
target.PropertyChanging -= handler;
};
});
}
public class ItemPropertyChangingEvent<TSender>
{
public TSender Sender { get; set; }
public PropertyInfo Property { get; set; }
public PropertyChangingEventArgs OriginalEventArgs { get; set; }
public override string ToString()
{
return string.Format("Sender: {0}, Property: {1}", Sender, Property);
}
}
public class ItemPropertyChangingEvent<TSender, TProperty>
{
public TSender Sender { get; set; }
public PropertyInfo Property { get; set; }
public PropertyChangingCancelEventArgs<TProperty> OriginalEventArgs { get; set; }
}
}
Then example usage will be like this
public class MainWindowViewModel : INPCBase
{
private string field1;
private string field2;
public MainWindowViewModel()
{
field1 = "Hello";
field2 = "World";
this.ObserveSpecificPropertyChanging(x => x.Field2)
.Subscribe(x =>
{
if (x.OriginalEventArgs.NewValue == "DOG")
{
x.OriginalEventArgs.Cancel = true;
}
});
}
public string Field1
{
get
{
return field1;
}
set
{
if (field1 != value)
{
if (this.OnPropertyChanging("Field1", field1, value))
{
var previousValue = field1;
field1 = value;
this.OnPropertyChanged("Field1", previousValue, value);
}
}
}
}
public string Field2
{
get
{
return field2;
}
set
{
if (field2 != value)
{
if (this.OnPropertyChanging("Field2", field2, value))
{
var previousValue = field2;
field2 = value;
this.OnPropertyChanged("Field2", previousValue, value);
}
}
}
}
}
Works a treat