Going through:
WPF binding not updating the view
https://learn.microsoft.com/en-us/dotnet/api/system.componentmodel.inotifypropertychanged?redirectedfrom=MSDN&view=netcore-3.1
and
WPF DataContext updated but UI not updated
I still can't see why the UI is not updating in the following case (my best guess is that the DataContext of the Grid to be updated is not updated) and am loosing my mind:
AppliedJobsModel.cs (has IPropertyChange implemented as some of the answers suggest):
public class AppliedJobsModel { }
public class AppliedJob : INotifyPropertyChanged
{
private string appliedDate;
private string url;
private string company;
private string description;
private string contact;
private string stack;
private string response;
private string interviewDate;
public AppliedJob(string[] entries)
{
appliedDate = entries[Consts.APPLIED_DATE_INDEX];
url = entries[Consts.URL_INDEX];
company = entries[Consts.COMPANY_INDEX];
description = entries[Consts.DESCRIPTION_INDEX];
contact = entries[Consts.CONTACT_INDEX];
stack = entries[Consts.STACK_INDEX];
response = entries[Consts.RESPONSE_INDEX];
interviewDate = entries[Consts.INTERVIEWDATE_INDEX];
}
public string AppliedDate
{
get {
return appliedDate;
}
set {
if (appliedDate != value)
{
appliedDate = value;
RaisePropertyChanged("AppliedDate");
}
}
}
public string Url
{
get
{
return url;
}
set
{
if (url != value)
{
url = value;
RaisePropertyChanged("Url");
}
}
}
public string Company
{
get
{
return company;
}
set
{
if (company != value)
{
company = value;
RaisePropertyChanged("Company");
}
}
}
public string Description
{
get
{
return description;
}
set
{
if (description != value)
{
description = value;
RaisePropertyChanged("Description");
}
}
}
public string Contact
{
get
{
return contact;
}
set
{
if (contact != value)
{
contact = value;
RaisePropertyChanged("Contact");
}
}
}
public string Stack
{
get
{
return stack;
}
set
{
if (stack != value)
{
stack = value;
RaisePropertyChanged("Stack");
}
}
}
public string Response
{
get
{
return response;
}
set
{
if (response != value)
{
response = value;
RaisePropertyChanged("Response");
}
}
}
public string InterviewDate
{
get
{
return interviewDate;
}
set
{
if (interviewDate != value)
{
interviewDate = value;
RaisePropertyChanged("InterviewDate");
}
}
}
public event PropertyChangedEventHandler PropertyChanged;
private void RaisePropertyChanged(string property)
{
if (PropertyChanged != null)
{
PropertyChanged(this, new PropertyChangedEventArgs(property));
}
}
}
AppliedJobsViewModel.cs (has an observable collection that gets correctly updated when a button is clicked (in dbg)):
class AppliedJobsViewModel
{
private TexParser texParser;
public AppliedJobsViewModel() {
// TODO:
// -- do nothing here
}
public ObservableCollection<AppliedJob> AppliedJobsCollection
{
get;
set;
}
private ICommand _openTexClick;
public ICommand OpenTexClick
{
get
{
return _openTexClick ?? (_openTexClick = new CommandHandler(() => ReadAndParseTexFile(), () => CanExecute));
}
}
public bool CanExecute
{
get
{
// check if executing is allowed, i.e., validate, check if a process is running, etc.
return true;
}
}
public async Task ReadAndParseTexFile()
{
if (texParser == null)
{
texParser = new TexParser();
}
// Read file asynchronously here
await Task.Run(() => ReadFileAndUpdateUI());
}
private void ReadFileAndUpdateUI()
{
texParser.ReadTexFile();
string[][] appliedJobsArray = texParser.getCleanTable();
// Use this:
// https://rachel53461.wordpress.com/2011/09/17/wpf-grids-rowcolumn-count-properties/
// Update collection here
List<AppliedJob> appliedJobsList = createAppliedJobsListFromTable(appliedJobsArray);
AppliedJobsCollection = new ObservableCollection<AppliedJob>(appliedJobsList);
}
private List<AppliedJob> createAppliedJobsListFromTable(string[][] table)
{
List<AppliedJob> jobsList = new List<AppliedJob>();
for (int i = 0; i < table.Length; i++)
{
jobsList.Add(new AppliedJob(table[i]));
}
return jobsList;
}
}
AppliedJobsView.xaml:
<UserControl x:Class="JobTracker.Views.AppliedJobsView"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:local="clr-namespace:JobTracker.Views"
mc:Ignorable="d"
d:DesignHeight="450" d:DesignWidth="800">
<Grid Name="appliedJobsGrid" Grid.Row="1" Grid.Column="1" Background="#50000000" Margin="10,10,10,10">
<ItemsControl ItemsSource = "{Binding Path = AppliedJobsCollection}">
<ItemsControl.ItemTemplate>
<DataTemplate>
<StackPanel Orientation = "Horizontal">
<TextBox Text = "{Binding Path = AppliedDate, Mode = TwoWay}" Width = "100" />
<TextBox Text = "{Binding Path = Url, Mode = TwoWay}" Width = "100" />
<TextBox Text = "{Binding Path = Company, Mode = TwoWay}" Width = "100" />
<TextBox Text = "{Binding Path = Description, Mode = TwoWay}" Width = "100" />
<TextBox Text = "{Binding Path = Contact, Mode = TwoWay}" Width = "100" />
<TextBox Text = "{Binding Path = Stack, Mode = TwoWay}" Width = "100" />
<TextBox Text = "{Binding Path = Response, Mode = TwoWay}" Width = "100" />
<TextBox Text = "{Binding Path = InterviewDate, Mode = TwoWay}" Width = "100" />
</StackPanel>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
</Grid>
TrackerHome.xaml (main page/uses the user control):
<Grid Grid.Row="1" Grid.Column="1">
<views:AppliedJobsView x:Name = "AppliedJobsControl" Loaded = "AppliedJobsViewControl_Loaded" />
</Grid>
TrackerHome.cs:
public TrackerHome()
{
InitializeComponent();
// Set data context here (https://stackoverflow.com/questions/12422945/how-to-bind-wpf-button-to-a-command-in-viewmodelbase)
// https://stackoverflow.com/questions/33929513/populate-a-datagrid-using-viewmodel-via-a-database
if (appliedJobsViewModel == null)
{
appliedJobsViewModel = new AppliedJobsViewModel();
}
this.DataContext = appliedJobsViewModel;
//AppliedJobControl.DataContext = appliedJobsViewModel;
}
private void AppliedJobsViewControl_Loaded(object sender, RoutedEventArgs e)
{
if (appliedJobsViewModel == null)
{
appliedJobsViewModel = new AppliedJobsViewModel();
}
AppliedJobsControl.DataContext = appliedJobsViewModel;
}
You are setting a new value of property here:
AppliedJobsCollection = new ObservableCollection<AppliedJob>(appliedJobsList);
but it's a simple auto-property without notification.
Make it full property (view model needs to implement INotifyPropertyChange):
ObservableCollection<AppliedJob> _appliedJobsCollection =
new ObservableCollection<AppliedJob>(); // empty initially
public ObservableCollection<AppliedJob> AppliedJobsCollection
{
get => _appliedJobsCollection;
set
{
_appliedJobsCollection = value;
RaisePropertyChanged(nameof(AppliedJobsCollection));
}
}
How does the full property behave? Is it as if all entries in each item in the collection have been changed (and thus have their properties changed)?
See this pseudo-code.
// given that AppliedJobsCollection is already initialized
// modify existing collection -> works
// bindings was subscribed to CollectionChanged event and will update
AppliedJobsCollection.Add(new AppliedJob(...));
// change item property -> works
// you implement INotifyPropertyChanged for items
// bindings was subscribed to that and will update
AppliedJobsCollection[0].Company = "bla";
// new instance of collection -> ... doesn't works
// how bindings can update?
AppliedJobsCollection = new ObservableCollection<AppliedJob>(...);
For last scenario to work you need to implement INotifyPropertyChanged for a class containing AppliedJobsCollection property and rise notification.
I am a student that just finished up a summer internship, and I brought home a project to work on briefly before school starts up. This project has a stopwatch in it, and I would rather use an ObservableCollection bound to my ListBox for my split times, rather that using the listbox.Items.Add(). When I add to the ObservableCollection, the ListBox UI does not update. Could anyone point me in the right direction on what I missed or what I did wrong?
I have my TimeSplits class:
public class TimeSplits : INotifyPropertyChanged
{
private int _hours;
private int _minutes;
private int _seconds;
public int hours
{
get
{
return _hours;
}
set
{
_hours = value;
NotifyPropertyChanged(hours);
}
}
public int minutes
{
get
{
return _minutes;
}
set
{
_minutes = value;
NotifyPropertyChanged(minutes);
}
}
public int seconds
{
get
{
return _seconds;
}
set
{
_seconds = value;
NotifyPropertyChanged(seconds);
}
}
public event PropertyChangedEventHandler PropertyChanged;
private void NotifyPropertyChanged(int propertyName)
{
if (PropertyChanged != null)
{
PropertyChanged(this, new PropertyChangedEventArgs(nameof(propertyName)));
}
}
public override string ToString()
{
return hours.ToString() + ":" + minutes.ToString() + ":" + seconds.ToString();
}
}
and my ObservableCollection in my Page:
public partial class StopwatchPage : Page , INotifyPropertyChanged
{
...
public ObservableCollection<TimeSplits> splits = new ObservableCollection<TimeSplits>();
...
public StopwatchPage()
{
DataContext = this;
InitializeComponent();
timer.Interval = TimeSpan.FromSeconds(1);
timer.Tick += new EventHandler(stopwatchTimer);
}
...
private void splitButton_Click(object sender, RoutedEventArgs e)
{
TimeSplits split = new TimeSplits();
split.hours = Hours;
split.minutes = Minutes;
split.seconds = Seconds;
splits.Add(split);
}
...
}
and my xaml:
<ListBox x:Name="newSplitListBox" HorizontalAlignment="Left" Margin="139,0,0,47" Width="185" Height="268" VerticalAlignment="Bottom" ItemsSource="{Binding splits}">
<ListBox.ItemTemplate>
<DataTemplate>
<StackPanel Orientation="Horizontal">
<TextBlock Text="{Binding hours}"/>
<TextBlock Text="{Binding minutes}"/>
<TextBlock Text="{Binding seconds}"/>
</StackPanel>
</DataTemplate>
</ListBox.ItemTemplate>
</ListBox>
I am sure it is something small that I have no clue about, as I just started learning data binding this summer. Any help is greatly appreciated! Thanks in advance.
It looks like you have nameof() in the wrong place. The way your current code reads, it will always send the value of "propertyName" as the name of the property that changed, regardless of what property actually changed.
Try this:
public int hours
{
get
{
return _hours;
}
set
{
_hours = value;
NotifyPropertyChanged();
}
}
Then, in your NotifyPropertyChanged(), do this:
private void NotifyPropertyChanged([CallerMemberName] string propertyName = "")
{
if (PropertyChanged != null)
{
PropertyChanged(this, new PropertyChangedEventArgs(propertyName);
}
}
Edit: Added fix for the following:
Also, the ObservableCollection needs to be a property. Change this code:
public ObservableCollection<TimeSplits> splits = new ObservableCollection<TimeSplits>();
To this:
public ObservableCollection<TimeSplits> Splits { get; set; } = new ObservableCollection<TimeSplits>();
I learned a trick from Xamarin's ViewModel template that helped me immensely. Here is the code that it generates that handles an observable View Model (much like the ObservableCollection).
protected bool SetProperty<T>(ref T backingStore, T value,
Action onChanged = null,
[CallerMemberName]string propertyName = "")
{
if (EqualityComparer<T>.Default.Equals(backingStore, value))
return false;
backingStore = value;
onChanged?.Invoke();
OnPropertyChanged(propertyName);
return true;
}
#region INotifyPropertyChanged
public event PropertyChangedEventHandler PropertyChanged;
protected void OnPropertyChanged([CallerMemberName] string propertyName = "")
{
var changed = PropertyChanged;
if (changed == null)
return;
changed.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
#endregion
Then, to use this, simply add this to your properties:
private string _title = string.Empty;
public string Title
{
get => _title;
set => SetProperty(ref _title, value);
}
I'm new to C# and I'm trying to create a code using MVVM pattern, but I don't know how to populate a combobox using that pattern. Please Give me help to create the ViewModel and the binding to the xaml.
Code Model:
public int Cd_Raca
{
get;
set
{
if(Cd_Raca != value)
{
Cd_Raca = value;
RaisePropertyChanged("Cd_Raca");
}
}
}
public string Nm_Raca
{
get;
set
{
if(Nm_Raca != value)
{
Nm_Raca = value;
RaisePropertyChanged("Nm_Raca");
}
}
}
public event PropertyChangedEventHandler PropertyChanged;
private void RaisePropertyChanged(string property)
{
if(PropertyChanged != null)
{
PropertyChanged(this, new PropertyChangedEventArgs(property));
}
}
}
Xaml:
<ComboBox x:Name="dsCmbRaca" HorizontalAlignment="Left" Margin="438,4,0,0"
VerticalAlignment="Top" Width="94" Height="19"/>
Use the ItemsSource Property and set it to an enumeration of objects. With DisplayMemberPath you can set it to a property of a single object of your list if the list is not just a list of strings.
I.e. in my sample the object of the list has a Description property for display and a Value property for the selected value.
All bindings in the sample need to be a property in your ViewModel (=DataContext).
<ComboBox DisplayMemberPath="Description" HorizontalAlignment="Left"
VerticalAlignment="Top" Width="120"
ItemsSource="{Binding myList}"
SelectedValue="{Binding mySelectedValue}" SelectedValuePath="Value" />
Edit:
The List property could look like this:
public IList<MyObject> myList { get { return new List<MyObject>();} }
The Object could look like this for example:
public class MyObject
{
public string Description { get; }
public enum Value { get;}
}
The Object is optional. You could just pass a list of strings.
Disclaimer: I hacked this in notepad. I hope it compiles.
UPDATE
Looking at your code at least from what you post your properties are not implemented correctly. You need a backing field if you code it like you have:
private int _cd_Raca;
private string _nm_Raca;
public int Cd_Raca
{
get{ return _cd_Raca;}
set
{
if(_cd_Raca != value)
{
_cd_Raca = value;
RaisePropertyChanged("Cd_Raca");
}
}
}
public string Nm_Raca
{
get{return _nm_Raca;}
set
{
if(_nm_Raca != value)
{
_nm_Raca = value;
RaisePropertyChanged("Nm_Raca");
}
}
}
public event PropertyChangedEventHandler PropertyChanged;
private void RaisePropertyChanged(string property)
{
if(PropertyChanged != null)
{
PropertyChanged(this, new PropertyChangedEventArgs(property));
}
}
Reading your comment to my first answer seems you might have a specific use case. So if this update does not help maybe you can add some more information to your question.
I'm new to WPF and MVVM and I'm developing a test WPF application following the MVVM design pattern. My database has 2 entities, Cards and Departments. Any card can have only 1 department, so it's a one-to-many relationship.
I've created the following ViewModel in order to bind to the view:
public class CardViewModel : INotifyPropertyChanged
{
public CardViewModel(Card card)
{
this.Card = card;
SqlConnectionStringBuilder builder = new SqlConnectionStringBuilder();
builder.DataSource = ".\\SQLExpress";
builder.InitialCatalog = "TESTDB";
builder.IntegratedSecurity = true;
SybaseDatabaseContext myDB = new SybaseDatabaseContext(builder.ConnectionString);
var query = from d in myDB.Departments
select d;
this.Departments = new ObservableCollection<Department>(query);
}
private Card _Card;
private ObservableCollection<Department> _Departments;
public Card Card
{
get { return _Card; }
set
{
if (value != this._Card)
{
this._Card = value;
SendPropertyChanged("Card");
}
}
}
public ObservableCollection<Department> Departments
{
get { return _Departments; }
set
{
this._Departments = value;
SendPropertyChanged("Departments");
}
}
#region INPC
// Logic for INotify interfaces that nootify WPF when change happens
public event PropertyChangedEventHandler PropertyChanged;
protected virtual void SendPropertyChanged(String propertyName)
{
if ((this.PropertyChanged != null))
{
this.PropertyChanged(this, new PropertyChangedEventArgs(propertyName));
}
}
#endregion
}
The CardForms' datacontext is currently being set to an instance of the CardViewModel in the code where the CardForm is being instantiated, but I'm going to create a IoC container or dependency injections down the line.
Everything binds correctly except for the ComboBox that should contain all departments and that has the current department in the Card instance selected (card.Department). Here's the XAML for the ComboBox:
<ComboBox Height="23" HorizontalAlignment="Left" Margin="350,64,0,0"
Name="comboBoxDepartment" VerticalAlignment="Top" Width="120"
IsSynchronizedWithCurrentItem="True"
ItemsSource="{Binding Path=Departments}"
DisplayMemberPath="DepartmentName"
SelectedItem="{Binding Path=Card.Department, Mode=TwoWay}" />
The departments are displayed in the combobox, but the current department of the card isn't and if I try to change it I get and error saying "Cannot add an entity with a key that is already in use".
So, my question is, how do I bind this combobox correctly to my ViewModel?
P.S. I know populating the ObservableCollection<Department> in the ViewModel is probably not the right way to do it, but I could not think of a better way at the time. If you have any suggestions for this also, please let me know.
Additionally, this is the Card model:
[Table(Name = "Card")]
public class Card : INotifyPropertyChanged, INotifyPropertyChanging
{
private string _CardID;
private string _Holder;
private Int16? _DepartmentNo;
[Column(UpdateCheck = UpdateCheck.WhenChanged)]
public string CardID
{
get
{
return this._CardID;
}
set
{
if (value != this._CardID)
{
SendPropertyChanging();
this._CardID = value;
SendPropertyChanged("CardID");
}
}
}
[Column(UpdateCheck = UpdateCheck.WhenChanged)]
public string Holder
{
get
{
return this._Holder;
}
set
{
if (value != this._Holder)
{
SendPropertyChanging();
this._Holder = value;
SendPropertyChanged("Holder");
}
}
}
[Column(CanBeNull = true, UpdateCheck = UpdateCheck.WhenChanged)]
public Int16? DepartmentNo
{
get
{
return this._DepartmentNo;
}
set
{
if (value != this._DepartmentNo)
{
SendPropertyChanging();
this._DepartmentNo = value;
SendPropertyChanged("DepartmentNo");
}
}
}
private EntityRef<Department> department;
[Association(Storage = "department", ThisKey = "DepartmentNo", OtherKey = "DepartmentNo", IsForeignKey = true)]
public Department Department
{
get
{
return this.department.Entity;
}
set
{
Department previousValue = this.department.Entity;
if (((previousValue != value)
|| (this.department.HasLoadedOrAssignedValue == false)))
{
this.SendPropertyChanging();
if ((previousValue != null))
{
this.department.Entity = null;
previousValue.Cards.Remove(this);
}
this.department.Entity = value;
if ((value != null))
{
value.Cards.Add(this);
this._DepartmentNo = value.DepartmentNo;
}
else
{
this._DepartmentNo = default(Nullable<short>);
}
this.SendPropertyChanged("Department");
}
}
}
I edited the constructor in the CardViewModel to take the DataContext as a parameter and that did it. This is the new CardViewModel constructor:
public CardViewModel(Card card, SybaseDatabaseContext myDB)
{
this.Card = card;
var query = from d in myDB.Departments
select d;
this.Departments = new ObservableCollection<Department>(query);
}
Had to do a bit of research on this myself. Thought I would contribute with a self answered question, but found this open current question...
The ComboBox is designed to be a kind of textbox that restricts it's possible values to the contents of a given list. The list is provided by the ItemsSource attribute. The current value of the ComboBox is the SelectedValue property. Typically these attributes are bound to relevant properties of a corresponding ViewModel.
The following example shows wired ComboBox together with a TextBox control used to redundantly view the current value of the ComboBox by sharing a view model property. (It is interesting to note that when TextBox changes the shared property to a value outside the scope of the ComboBox's list of values, the ComboBox displays nothing.)
Note: the following WPF/C# example does does use code-behind and so presents the ViewModel as merely the datacontext of the view and not a partial class of it, a current implementation constraint when using WPF with F#.
WPF XAML
<Window
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:m="clr-namespace:WpfApplication1"
Title="MainWindow" Height="350" Width="525">
<Window.DataContext>
<m:MainWindowVM />
</Window.DataContext>
<StackPanel>
<TextBox Text="{Binding SelectedString}" />
<ComboBox ItemsSource="{Binding MyList}" SelectedValue="{Binding SelectedString}" />
</StackPanel>
</Window>
C# ViewModel
using System.Collections.Generic;
using System.ComponentModel;
namespace WpfApplication1
{
public class MainWindowVM : INotifyPropertyChanged
{
string selectedString;
void NotifyPropertyChanged(string propertyName)
{
if (PropertyChanged == null) return;
PropertyChanged(this, new PropertyChangedEventArgs(propertyName));
}
public string SelectedString
{
get { return selectedString; }
set
{
selectedString = value;
NotifyPropertyChanged("SelectedString");
}
}
public List<string> MyList
{
get { return new List<string> { "The", "Quick", "Brown", "Fox" }; }
}
public event PropertyChangedEventHandler PropertyChanged;
}
}
By default, ToString() is used to interpret the objects in the list. However, ComboBox offers DisplayMemberPath and SelectedValuePath attributes for specifying paths to specific object properties for corresponding displayed and stored values. These paths are relative to the list object element so a path of "Name" refers to Name on a list object item.
The "Remarks" section of this MSDN link explains the interpretations of the IsEditable and IsReadOnly ComboBox properties.
Being new to WPF & MVVM I struggling with some basic functionality.
Let me first explain what I am after, and then attach some example code...
I have a screen showing a list of users, and I display the details of the selected user on the right-hand side with editable textboxes. I then have a Save button which is DataBound, but I would only like this button to display when data has actually changed. ie - I need to check for "dirty data".
I have a fully MVVM example in which I have a Model called User:
namespace Test.Model
{
class User
{
public string UserName { get; set; }
public string Surname { get; set; }
public string Firstname { get; set; }
}
}
Then, the ViewModel looks like this:
using System.Collections.ObjectModel;
using System.Collections.Specialized;
using System.Windows.Input;
using Test.Model;
namespace Test.ViewModel
{
class UserViewModel : ViewModelBase
{
//Private variables
private ObservableCollection<User> _users;
RelayCommand _userSave;
//Properties
public ObservableCollection<User> User
{
get
{
if (_users == null)
{
_users = new ObservableCollection<User>();
//I assume I need this Handler, but I am stuggling to implement it successfully
//_users.CollectionChanged += HandleChange;
//Populate with users
_users.Add(new User {UserName = "Bob", Firstname="Bob", Surname="Smith"});
_users.Add(new User {UserName = "Smob", Firstname="John", Surname="Davy"});
}
return _users;
}
}
//Not sure what to do with this?!?!
//private void HandleChange(object sender, NotifyCollectionChangedEventArgs e)
//{
// if (e.Action == NotifyCollectionChangedAction.Remove)
// {
// foreach (TestViewModel item in e.NewItems)
// {
// //Removed items
// }
// }
// else if (e.Action == NotifyCollectionChangedAction.Add)
// {
// foreach (TestViewModel item in e.NewItems)
// {
// //Added items
// }
// }
//}
//Commands
public ICommand UserSave
{
get
{
if (_userSave == null)
{
_userSave = new RelayCommand(param => this.UserSaveExecute(), param => this.UserSaveCanExecute);
}
return _userSave;
}
}
void UserSaveExecute()
{
//Here I will call my DataAccess to actually save the data
}
bool UserSaveCanExecute
{
get
{
//This is where I would like to know whether the currently selected item has been edited and is thus "dirty"
return false;
}
}
//constructor
public UserViewModel()
{
}
}
}
The "RelayCommand" is just a simple wrapper class, as is the "ViewModelBase". (I'll attach the latter though just for clarity)
using System;
using System.ComponentModel;
namespace Test.ViewModel
{
public abstract class ViewModelBase : INotifyPropertyChanged, IDisposable
{
protected ViewModelBase()
{
}
public event PropertyChangedEventHandler PropertyChanged;
protected virtual void OnPropertyChanged(string propertyName)
{
PropertyChangedEventHandler handler = this.PropertyChanged;
if (handler != null)
{
var e = new PropertyChangedEventArgs(propertyName);
handler(this, e);
}
}
public void Dispose()
{
this.OnDispose();
}
protected virtual void OnDispose()
{
}
}
}
Finally - the XAML
<Window x:Class="Test.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:vm="clr-namespace:Test.ViewModel"
Title="MainWindow" Height="350" Width="525">
<Window.DataContext>
<vm:UserViewModel/>
</Window.DataContext>
<Grid>
<ListBox Height="238" HorizontalAlignment="Left" Margin="12,12,0,0" Name="listBox1" VerticalAlignment="Top"
Width="197" ItemsSource="{Binding Path=User}" IsSynchronizedWithCurrentItem="True">
<ListBox.ItemTemplate>
<DataTemplate>
<StackPanel>
<TextBlock Text="{Binding Path=Firstname}"/>
<TextBlock Text="{Binding Path=Surname}"/>
</StackPanel>
</DataTemplate>
</ListBox.ItemTemplate>
</ListBox>
<Label Content="Username" Height="28" HorizontalAlignment="Left" Margin="232,16,0,0" Name="label1" VerticalAlignment="Top" />
<TextBox Height="23" HorizontalAlignment="Left" Margin="323,21,0,0" Name="textBox1" VerticalAlignment="Top" Width="120" Text="{Binding Path=User/UserName}" />
<Label Content="Surname" Height="28" HorizontalAlignment="Left" Margin="232,50,0,0" Name="label2" VerticalAlignment="Top" />
<TextBox Height="23" HorizontalAlignment="Left" Margin="323,52,0,0" Name="textBox2" VerticalAlignment="Top" Width="120" Text="{Binding Path=User/Surname}" />
<Label Content="Firstname" Height="28" HorizontalAlignment="Left" Margin="232,84,0,0" Name="label3" VerticalAlignment="Top" />
<TextBox Height="23" HorizontalAlignment="Left" Margin="323,86,0,0" Name="textBox3" VerticalAlignment="Top" Width="120" Text="{Binding Path=User/Firstname}" />
<Button Content="Button" Height="23" HorizontalAlignment="Left" Margin="368,159,0,0" Name="button1" VerticalAlignment="Top" Width="75" Command="{Binding Path=UserSave}" />
</Grid>
</Window>
So basically, when I edit a surname, the Save button should be enabled; and if I undo my edit - well then it should be Disabled again as nothing has changed.
I have seen this in many examples, but have not yet found out how to do it.
Any help would be much appreciated!
Brendan
In my experience, if you implement IsDirty in your view model, you probably also want the view model to implement IEditableObject.
Assuming that your view model is the usual sort, implementing PropertyChanged and a private or protected OnPropertyChanged method that raises it, setting IsDirty is simple enough: you just set IsDirty in OnPropertyChanged if it isn't already true.
Your IsDirty setter should, if the property was false and is now true, call BeginEdit.
Your Save command should call EndEdit, which updates the data model and sets IsDirty to false.
Your Cancel command should call CancelEdit, which refreshes the view model from the data model and sets IsDirty to false.
The CanSave and CanCancel properties (assuming you're using a RelayCommand for these commands) just return the current value of IsDirty.
Note that since none of this functionality depends on the specific implementation of the view model, you can put it in an abstract base class. Derived classes don't have to implement any of the command-related properties or the IsDirty property; they just have to override BeginEdit, EndEdit, and CancelEdit.
I've done some work on implementing IsDirty for models that is wrapped in my ViewModel.
The result really simplified my ViewModels:
public class PersonViewModel : ViewModelBase
{
private readonly ModelDataStore<Person> data;
public PersonViewModel()
{
data = new ModelDataStore<Person>(new Person());
}
public PersonViewModel(Person person)
{
data = new ModelDataStore<Person>(person);
}
#region Properties
#region Name
public string Name
{
get { return data.Model.Name; }
set { data.SetPropertyAndRaisePropertyChanged("Name", value, this); }
}
#endregion
#region Age
public int Age
{
get { return data.Model.Age; }
set { data.SetPropertyAndRaisePropertyChanged("Age", value, this); }
}
#endregion
#endregion
}
Code # http://wpfcontrols.codeplex.com/
Check under the Patterns assembly and MVVM folder, you'll find a ModelDataStore class.
P.S.
I haven't done a full scale test on it, just the really simple test you'll find the Test assembly.
I would suggest you to use GalaSoft MVVM Light Toolkit as it is much more easier to implement than DIY approach.
For dirty reads, you need to keep the snapshot of each fields, and return true or false from UserSaveCanExecute() method, which will enable / disable command button accordingly.
If you wanted to take a framework approach rather than writing the infrastructure yourself, you could use CSLA (http://www.lhotka.net/cslanet/) - Rocky's framework for developing business objects. Object state is managed for you on property changes, and the code base also includes an example ViewModel type which supports an underlying model, a Save verb, and a CanSave property. You may be able to take inspiration from the code, even you didn't want to use the framework.
I have come up with a working solution. This may of course not be the best way, but I am sure I can work on it as I learn more...
When I run the project, if I cange any item, the list box is disabled, and the save button enabled. If I undo my edits, then the list box is enabled again, and the save button disabled.
I have changed my User Model to implement INotifyPropertyChanged, and I have also created a set of private variables to store the "original values" and some logic to check for "IsDirty"
using System.ComponentModel;
namespace Test.Model
{
public class User : INotifyPropertyChanged
{
//Private variables
private string _username;
private string _surname;
private string _firstname;
//Private - original holders
private string _username_Orig;
private string _surname_Orig;
private string _firstname_Orig;
private bool _isDirty;
//Properties
public string UserName
{
get
{
return _username;
}
set
{
if (_username_Orig == null)
{
_username_Orig = value;
}
_username = value;
SetDirty();
}
}
public string Surname
{
get { return _surname; }
set
{
if (_surname_Orig == null)
{
_surname_Orig = value;
}
_surname = value;
SetDirty();
}
}
public string Firstname
{
get { return _firstname; }
set
{
if (_firstname_Orig == null)
{
_firstname_Orig = value;
}
_firstname = value;
SetDirty();
}
}
public bool IsDirty
{
get
{
return _isDirty;
}
}
public void SetToClean()
{
_username_Orig = _username;
_surname_Orig = _surname;
_firstname_Orig = _firstname;
_isDirty = false;
OnPropertyChanged("IsDirty");
}
private void SetDirty()
{
if (_username == _username_Orig && _surname == _surname_Orig && _firstname == _firstname_Orig)
{
if (_isDirty)
{
_isDirty = false;
OnPropertyChanged("IsDirty");
}
}
else
{
if (!_isDirty)
{
_isDirty = true;
OnPropertyChanged("IsDirty");
}
}
}
public event PropertyChangedEventHandler PropertyChanged;
protected void OnPropertyChanged(string propertyName)
{
PropertyChangedEventHandler handler = PropertyChanged;
if (handler != null)
{
handler(this, new PropertyChangedEventArgs(propertyName));
}
}
}
Then, my ViewModel has changed a bit too....
using System.Collections.ObjectModel;
using System.Collections.Specialized;
using System.Windows.Input;
using Test.Model;
using System.ComponentModel;
namespace Test.ViewModel
{
class UserViewModel : ViewModelBase
{
//Private variables
private ObservableCollection<User> _users;
RelayCommand _userSave;
private User _selectedUser = new User();
//Properties
public ObservableCollection<User> User
{
get
{
if (_users == null)
{
_users = new ObservableCollection<User>();
_users.CollectionChanged += (s, e) =>
{
if (e.Action == NotifyCollectionChangedAction.Add)
{
// handle property changing
foreach (User item in e.NewItems)
{
((INotifyPropertyChanged)item).PropertyChanged += (s1, e1) =>
{
OnPropertyChanged("EnableListBox");
};
}
}
};
//Populate with users
_users.Add(new User {UserName = "Bob", Firstname="Bob", Surname="Smith"});
_users.Add(new User {UserName = "Smob", Firstname="John", Surname="Davy"});
}
return _users;
}
}
public User SelectedUser
{
get { return _selectedUser; }
set { _selectedUser = value; }
}
public bool EnableListBox
{
get { return !_selectedUser.IsDirty; }
}
//Commands
public ICommand UserSave
{
get
{
if (_userSave == null)
{
_userSave = new RelayCommand(param => this.UserSaveExecute(), param => this.UserSaveCanExecute);
}
return _userSave;
}
}
void UserSaveExecute()
{
//Here I will call my DataAccess to actually save the data
//Save code...
_selectedUser.SetToClean();
OnPropertyChanged("EnableListBox");
}
bool UserSaveCanExecute
{
get
{
return _selectedUser.IsDirty;
}
}
//constructor
public UserViewModel()
{
}
}
Finally, the XAML
I changed the bindings on the Username, Surname & Firstname to include UpdateSourceTrigger=PropertyChanged
And then I bound the listbox's SelectedItem and IsEnabled
As I said in the beginning - it may not be the best solution, but it seems to work...
Since your UserSave command is in the ViewModel, I would do the tracking of the "dirty" state there. I would databind to the selected item in the ListBox, and when it changes, store a snapshot of the current values of the selected user's properties. Then you can compare to this to determine if the command should be enabled/disabled.
However, since you are binding directly to the model, you need some way to find out if something changed. Either you also implement INotifyPropertyChanged in the model, or wrap the properties in a ViewModel.
Note that when the CanExecute of the command changes, you may need to fire CommandManager.InvalidateRequerySuggested().
This is how I have implemented IsDirty. Create a wrapper for every property of User class (inheriting User class with IPropertyChanged and implementing onpropertychanged in User class wont help) in your ViewModal. You need to change your binding from UserName to WrapUserName.
public string WrapUserName
{
get
{
return User.UserName
}
set
{
User.UserName = value;
OnPropertyChanged("WrapUserName");
}
}
Now have a property
public bool isPageDirty
{
get;
set;
}
Since your viewmodal inherits from baseviewmodal and baseviewmodal implements onPropertyChanged.
UserViewModel.PropertyChanged += (s, e) => { isPageDirty = true; };
In case any of the propertychanges,isPageDirty will be true, So while saving you chan check isPageDirty.