Textboxes are not updated when the ViewModel's data changes - c#

I have implemented my first WPF MVVM Window. On loading the window a method is called that fetches data from an Access database, and all appropriate textboxes are bound to the EmployeeModel's properties.
I also have a button called Refresh that runs the same method to fetch the Employee data from the database, for testing purposes I have made the Refresh button fetch a different employee ID.
However none of the window's fields update with the new EmployeeModel. I've used breakpoints to find there is no issue saving the new data to the EmployeeModel, but no textbox appear to be updated with this new data.
Following guides online, I have used INotifyProperyChanged and use UpdateSourceTrigger in my view.
EmployeeModel
class EmployeeModel : MyDbConnector, INotifyPropertyChanged
{
private int _employeeId;
private string _firstname;
public int EmployeeId
{
get => _employeeId;
set
{
if (_employeeId != value)
{
_employeeId = value;
OnPropertyChanged();
}
}
}
public string Firstname
{
get => _firstname;
set
{
if (_firstname != value)
{
_firstname = value;
OnPropertyChanged();
}
}
}
public event PropertyChangedEventHandler PropertyChanged;
private void OnPropertyChanged([System.Runtime.CompilerServices.CallerMemberName]string caller = null)
{
if (PropertyChanged != null)
PropertyChanged(this, new PropertyChangedEventArgs(caller));
}
public static EmployeeModel FindById(int id)
{
//code omitted
}
EmployeeViewModel
class EmployeeViewModel
{
public EmployeeModel Employee { get; set; }
public EmployeeViewModel()
{
LoadEmployee(100);
}
public bool LoadEmployee(int employeeId)
{
if (!DbConnector.OpenDB())
return false;
Employee = EmployeeModel.FindById(employeeId);
DbConnector.CloseDB();
if (Employee == null)
return false;
return true;
}
public void Refresh()
{
// Load a different employee
if (!LoadEmployee(102))
MessageBox.Show("An error has occurred");
}
}
View
<Label Width="100" Content="Employee ID"/>
<TextBox Width="100" IsEnabled="False" Text="{Binding Employee.EmployeeId, UpdateSourceTrigger=PropertyChanged}"/>
<Label Width="100" Content="First name"/>
<TextBox Text="{Binding Employee.Firstname, TargetNullValue='', UpdateSourceTrigger=PropertyChanged}"/>

You're changing the entire Model for a new Model object but you're not notifying the UI that you did it. Your EmployeeViewModel class should implement INotifyPropertyChanged just like you did with the employee model and then the property Employee should call OnPropertyChanged method
private EmployeeModel _employee;
public EmployeeModel Employee {
get { return _employee; }
set {
if(_employee!=value) {
_employee=value;
OnPropertyChanged();
}
}
}

Related

How to disable a button if textbox and passwordbox is blank in wpf?

I basically used a Model's (UserAccount) Property from my ViewModel(CreateAccountViewModel) to bind to my View, and call to my Command (CreateAccountCommand).
My Model(UserAccount):
public class UserAccount : INotifyPropertyChanged
{
public event PropertyChangedEventHandler PropertyChanged;
private int _id;
private string _username;
private string _password;
private DateTime _dateTime;
public int Id
{
get { return _id; }
set { _id = value; OnPropertyChanged(nameof(Id)); }
}
public string Username
{
get { return _username; }
set { _username = value; OnPropertyChanged(nameof(Username)); }
}
public string Password
{
get { return _password; }
set { _password = value; OnPropertyChanged(nameof(Password)); }
}
public DateTime DateCreated
{
get { return _dateTime; }
set { _dateTime = value; OnPropertyChanged(nameof(DateCreated)); }
}
public virtual void OnPropertyChanged(string propertyName)
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
}
My ViewModel(CreateAccountViewModel):
public class CreateAccountViewModel: ViewModelBase
{
private UserAccount _userAccount;
public UserAccount CurrentUserAccount
{
get { return _userAccount; }
set { _userAccount = value; OnPropertyChanged(nameof(CurrentUserAccount)); }
}
public ICommand CreateAccountCommand{ get; }
public CreateAccountViewModel()
{
CreateAccountCommand= new CreateAccountCommand(this, Test);
CurrentUserAccount = new UserAccount();
}
public void Test()
{
MessageBox.Show("Random Message");
//I'm going to put my Create functionality here
}
}
My View (CreateAccountView):
<!--The TextBox for username-->
<TextBox Grid.Column="1"
Margin="10,0,0,0"
Text="{Binding Path=CurrentUserAccount.Username, UpdateSourceTrigger=PropertyChanged}" />
<!--The PasswordBox for password-->
<components:BindablePasswordBox Grid.Column="1"
Margin="10,0,0,0"
Password="{Binding Path=CurrentUserAccount.Password, UpdateSourceTrigger=PropertyChanged, Mode=TwoWay}"
Grid.ColumnSpan="2" />
<!--The Create user button-->
<Button Grid.Row="2"
Margin="0,20,0,0"
HorizontalAlignment="Center"
Command="{Binding CreateAccountCommand}"
Content="Create Account" />
My Command(CreateAccountCommand):
public class CreateAccountCommand: ICommand
{
private readonly CreateAccountViewModel _viewModel;
private readonly Action RunCommand;
public CreateAccountCommand(CreateAccountViewModel viewModel , Action runCommand)
{
_viewModel = viewModel;
_viewModel.PropertyChanged += ViewModel_PropertyChanged;
RunCommand = runCommand;
}
public event EventHandler CanExecuteChanged;
public bool CanExecute(object parameter)
{
//This is supposed to check whether the Username textbox and Password passwordbox is blank (if both of them are blank, the button should be disabled, else disabled
return !string.IsNullOrEmpty(_viewModel.CurrentUserAccount.Username) && !string.IsNullOrEmpty(_viewModel.CurrentUserAccount.Password);
}
public void Execute(object parameter)
{
RunCommand();
}
private void ViewModel_PropertyChanged(object sender, PropertyChangedEventArgs e)
{
CanExecuteChanged?.Invoke(this, new EventArgs());
}
}
My PasswordBox is bindable because I created a custom PasswordBox with DependencyProperty:
public partial class BindablePasswordBox : UserControl
{
public static readonly DependencyProperty PasswordProperty =
DependencyProperty.Register("Password", typeof(string), typeof(BindablePasswordBox),
new PropertyMetadata(string.Empty));
public string Password
{
get { return (string)GetValue(PasswordProperty); }
set { SetValue(PasswordProperty, value); }
}
public BindablePasswordBox()
{
InitializeComponent();
}
//This method will notify us, whenever a password in our passwordBox changes
private void PasswordBox_PasswordChanged(object sender, RoutedEventArgs e)
{
Password = passwordBox.Password; //sets the value of the DependencyProperty (PasswordProperty)
}
}
My problem here, is that, the button in my View does not change enable/disable even if I set my command's CanExecute to do so. Am I missing something obvious here? I really have to ask because I've been stuck here since yesterday. (My Main goal here is to disable the Create Account button if the Textbox and PasswordBox have no input. Any solutions are okay)
Lets do a small refactoring.
use CallerMemberNameAttribute (see here how) to have shorter property setters in vm;
write once reusable ICommand implementation and use it for all commands, see DelegateCommand;
rise command CanExecuteChanged in vm when you change one of command canExecuted condition;
UserAccount needs notifications (you have done it in the edit), if it's a model, then you need an extra vm to act as a wrapper, otherwise you wouldn't be able to catch changes done by the bound controls;
Since the properties of UserAccount are part of command canExecuted, you need to monitor for them.
With all changes your button using the command should be property enabled/disabled.
Below is pseudo-code (can contain mistakes):
public class CreateAccountViewModel: ViewModelBase
{
UserAccount _userAccount;
public UserAccount CurrentUserAccount
{
get => _userAccount;
set
{
// unsubscribe
if(_userAccount != null)
_userAccount -= UserAccount_PropertyChanged;
_userAccount = value;
// subscribe
if(_userAccount != null)
_userAccount += UserAccount_PropertyChanged;
// notifications
OnPropertyChanged(); // shorter syntax with CallerMemberNameAttribute
CreateAccountCommand.RaiseCanExecuteChanged();
}
}
public ICommand CreateAccountCommand { get; }
public CreateAccountViewModel()
{
CurrentUserAccount = new UserAccount();
CreateAccountCommand = new DelegateCommand(Test,
o => !string.IsNullOrEmpty(CurrentUserAccount.Username) && !string.IsNullOrEmpty(CurrentUserAccount.Password));
}
void Test(object parameter)
{
MessageBox.Show("Random Message");
//I'm going to put my Create functionality here
}
void UserAccount_PropertyChanged(object sender, NotifyPropertyChangedEventArgs e) =>
CreateAccountCommand.RaiseCanExecuteChanged(); // rise always of check for specific properties changes
}
The CreateAccountCommand hooks up en event handler to the view model's PropertyChanged but there is no such event raised when you set the Username and Password properties of the UserAccount object.
Either implement INotifyPropertyChanged in UserAccount or bind to wrapper properties of the CreateAccountViewModel:
public string Username
{
get { return _userAccount?.Username; }
set
{
if (_userAccount != null)
_userAccount.Username = value;
OnPropertyChanged();
}
}
If you decide to implement INotifyPropertyChanged in UserAccount, you still need to notify the command when the properties have been updated.
Since your CurrentUserAccount property may be set to a new value dynamically, you should remove and add the event handler dynamically:
private UserAccount _userAccount;
public UserAccount CurrentUserAccount
{
get { return _userAccount; }
set
{
if (_userAccount != null)
_userAccount.PropertyChanged -= OnUserAccountPropertyChanged;
_userAccount = value;
if (_userAccount != null)
_userAccount.PropertyChanged += OnUserAccountPropertyChanged;
OnPropertyChanged(nameof(CurrentUserAccount));
}
}
private void OnUserAccountPropertyChanged(object sender, PropertyChangedEventArgs e) =>
OnPropertyChanged(null);

how to use relational tables in a WPF viewmodel

I'm building a N tier WPF app. I want zero codebehind.
Let's say I have 3 normalized related tables to record sales transactions.
TRANSACTIONS:
TransactionId,
ItemId,
SupplierId,
Price
SUPPLIERS:
SupplierId,
SupplierName
ITEMS:
ItemId,
ItemName.
For each table I have a Base class that reflects the fields. Then a data layer that populates a collection of base objects as required.
I want to have a Listbox on the page showing a list of all of the transactions, 1 transaction per row, the rows should look something like this...
"Trainers FootLocker €99"
"Trousers TopShop €45"
"Coat TopShop €49"
If I use the
<ListBox
ItemsSource="{Binding Path=Transactions}"
SelectedItem="{Binding CurrentTransaction}"
then I end up with rows of IDs from the Transactions table and not the Name values from the Items and Suppliers tables.
Given that I have collection of transactions filled with only IDs to the other tables, what is the best approach to populating the listbox?
One thing I'm wondering is, should my Transactions Base object contain Item item populated there instead of int ItemId?
Transaction Base Model:
using System;
using System.ComponentModel;
using PFT;
using PFT.Data;
namespace PFT.Base
{
public class Transaction : INotifyPropertyChanged
{
public int Id { get; set; }
private int _itemId;
public int ItemId
{
get { return _itemId; }
set {
_itemId = value;
ItemData id = new ItemData();
this.Item = id.Select(value);
NotifyPropertyChanged("ItemId");
}
}
private Item _item;
public Item Item
{
get { return _item; }
set { _item = value; }
}
private float _price;
public float Price
{
get { return _price; }
set {
_price = value;
NotifyPropertyChanged("Price");
}
}
private DateTime _date;
public DateTime Date
{
get { return _date; }
set {
_date = value;
NotifyPropertyChanged("Date");
}
}
private string _comment;
public string Comment
{
get { return _comment; }
set
{
_comment = value;
NotifyPropertyChanged("Comment");
}
}
private int _traderId;
public int TraderId
{
get { return _traderId; }
set
{
_traderId = value;
NotifyPropertyChanged("TraderId");
}
}
private Trader _trader;
public Trader Trader
{
get { return _trader; }
set { _trader = value;
TraderData t = new TraderData();
this.Trader = t.Select(value);
}
}
private string _insertType;
/// <summary>
/// A - Auto, M - Manual, V - Verified
/// </summary>
public string InsertType
{
get { return _insertType; }
set { _insertType = value;
NotifyPropertyChanged("InsertType");
}
}
public event PropertyChangedEventHandler PropertyChanged;
// This method is called by the Set accessor of each property.
// The CallerMemberName attribute that is applied to the optional propertyName
// parameter causes the property name of the caller to be substituted as an argument.
//private void NotifyPropertyChanged([CallerMemberName] String propertyName = "")
private void NotifyPropertyChanged(String propertyName = "")
{
if (PropertyChanged != null)
{
PropertyChanged(this, new PropertyChangedEventArgs(propertyName));
}
}
}
ITEMS BASE CLASS
using System.ComponentModel;
namespace PFT.Base
{
public class Item : INotifyPropertyChanged
{
private int _id;
public int Id
{
get { return _id; }
set { _id = value;
NotifyPropertyChanged("Id");
}
}
private string _name;
public string Name
{
get { return _name; }
set { _name = value;
NotifyPropertyChanged("Name");
}
}
private string _description;
public string Description
{
get { return _description; }
set { _description = value;
NotifyPropertyChanged("Description");
}
}
private float _defaultPrice;
public float DefaultPrice
{
get { return _defaultPrice; }
set { _defaultPrice = value;
NotifyPropertyChanged("DefaultPrice");
}
}
private bool _isIncome;
public bool IsIncome
{
get { return _isIncome; }
set { _isIncome = value;
NotifyPropertyChanged("IsIncome");
}
}
public event PropertyChangedEventHandler PropertyChanged;
private void NotifyPropertyChanged(string propertyName = "")
{
if (PropertyChanged != null)
{
PropertyChanged(this, new PropertyChangedEventArgs(propertyName));
}
}
}
}
The way you'd do this with viewmodels would be to give Transaction a Supplier property and an Item property. Those properties would be references to the actual Item and Supplier objects in their own collections. If the relationship is one ItemID and one SupplierID per transaction, that's the object equivalent. If a transaction can be multiple records with the same transaction ID and different supplier or item IDs, then Transaction needs collections of Item and Supplier. We can do that in WPF too, but it'll take a lot more angle brackets than the trivial example below.
You would set that up when you get your items from the database (however you're doing that), or maybe Entity Framework can do that for you.
Real simple listbox displaying item names: Add DisplayMemberPath.
<ListBox
ItemsSource="{Binding Transactions}"
SelectedItem="{Binding CurrentTransaction}"
DisplayMemberPath="Item.Name"
/>
More complicated:
<ListBox
ItemsSource="{Binding Transactions}"
SelectedItem="{Binding CurrentTransaction}"
>
<ListBox.ItemTemplate>
<DataTemplate>
<TextBlock>
<Run Text="{Binding Item.Name, Mode=OneWay}" />
<Run Text=" - " />
<Run Text="{Binding Supplier.Name, Mode=OneWay}" />
<Run Text=" " />
<Run Text="{Binding Price, Mode=OneWay, StringFormat=c}" />
</TextBlock>
</DataTemplate>
</ListBox.ItemTemplate>
</ListBox>
You could also look into a columned control like a ListView or DataGrid.
Slightly off topic, zero code-behind is a bit extreme. It's a last resort, not a third rail. Minimal code-behind is a sound general principle. Don't go crazy trying to avoid it; it's there for a reason.
UPDATE
public class Transaction : INotifyPropertyChanged
{
// ... stuff ...
public Item Item
{
get { return _item; }
set {
_item = value;
// If this property is ever set outside the Transaction
// constructor, you ABSOLUTELY MUST raise PropertyChanged here.
// Otherwise, make the setter private. But just raise the event.
// This has nothing whatsoever to do with when or whether the Item
// class raises PropertyChanged, because this is not a property of the
// Item class. This is a property of Transaction.
NotifyPropertyChanged("Item");
}
}
// ... more stuff ...

WPF- How to update the changes in list item of a list

i have updated a list item of a list.The item is successfully updated at source i.e database ,but the list is not getting updated with updated item.I have used INotifyPropertyChanged interface for the list items and the list is binded to an observable collection.
private tbl_Model _modelItem;
public tbl_Model ModelItem
{
get { return _modelItem; }
private set
{
_modelItem = value;
NotifyPropertyChanged("ModelItem");
}
}
private ObservableCollection<tbl_Model> _modelCollection;
public ObservableCollection<tbl_Model> ModelCollection
{
get { return _modelCollection; }
private set
{
_modelCollection = value;
NotifyPropertyChanged("ModelCollection");
}
}
public void btn_update()
{
//Code to update at database
//what should i write here to update the list ?
}
As you can see in image,the list shows model no. as 101 even after i updated it to 102
Thanks in advance
The auto generated model via Linq to Sql
public partial class tbl_Model : INotifyPropertyChanging, INotifyPropertyChanged
{
private static PropertyChangingEventArgs emptyChangingEventArgs = new PropertyChangingEventArgs(String.Empty);
private int _ID;
private string _Model_No;
private string _Name;
private string _Manufacturer;
private int _IsDelete;
#region Extensibility Method Definitions
partial void OnLoaded();
partial void OnValidate(System.Data.Linq.ChangeAction action);
partial void OnCreated();
partial void OnIDChanging(int value);
partial void OnIDChanged();
partial void OnModel_NoChanging(string value);
partial void OnModel_NoChanged();
partial void OnNameChanging(string value);
partial void OnNameChanged();
partial void OnManufacturerChanging(string value);
partial void OnManufacturerChanged();
partial void OnIsDeleteChanging(int value);
partial void OnIsDeleteChanged();
#endregion
public tbl_Model()
{
OnCreated();
}
[global::System.Data.Linq.Mapping.ColumnAttribute(Storage="_ID", AutoSync=AutoSync.OnInsert, DbType="Int NOT NULL IDENTITY", IsPrimaryKey=true, IsDbGenerated=true)]
public int ID
{
get
{
return this._ID;
}
set
{
if ((this._ID != value))
{
this.OnIDChanging(value);
this.SendPropertyChanging();
this._ID = value;
this.SendPropertyChanged("ID");
this.OnIDChanged();
}
}
}
[global::System.Data.Linq.Mapping.ColumnAttribute(Storage="_Model_No", DbType="VarChar(50) NOT NULL", CanBeNull=false)]
public string Model_No
{
get
{
return this._Model_No;
}
set
{
if ((this._Model_No != value))
{
this.OnModel_NoChanging(value);
this.SendPropertyChanging();
this._Model_No = value;
this.SendPropertyChanged("Model_No");
this.OnModel_NoChanged();
}
}
}
[global::System.Data.Linq.Mapping.ColumnAttribute(Storage="_Name", DbType="VarChar(50) NOT NULL", CanBeNull=false)]
public string Name
{
get
{
return this._Name;
}
set
{
if ((this._Name != value))
{
this.OnNameChanging(value);
this.SendPropertyChanging();
this._Name = value;
this.SendPropertyChanged("Name");
this.OnNameChanged();
}
}
}
[global::System.Data.Linq.Mapping.ColumnAttribute(Storage="_Manufacturer", DbType="VarChar(50) NOT NULL", CanBeNull=false)]
public string Manufacturer
{
get
{
return this._Manufacturer;
}
set
{
if ((this._Manufacturer != value))
{
this.OnManufacturerChanging(value);
this.SendPropertyChanging();
this._Manufacturer = value;
this.SendPropertyChanged("Manufacturer");
this.OnManufacturerChanged();
}
}
}
[global::System.Data.Linq.Mapping.ColumnAttribute(Storage="_IsDelete", DbType="Int NOT NULL")]
public int IsDelete
{
get
{
return this._IsDelete;
}
set
{
if ((this._IsDelete != value))
{
this.OnIsDeleteChanging(value);
this.SendPropertyChanging();
this._IsDelete = value;
this.SendPropertyChanged("IsDelete");
this.OnIsDeleteChanged();
}
}
}
public event PropertyChangingEventHandler PropertyChanging;
public event PropertyChangedEventHandler PropertyChanged;
protected virtual void SendPropertyChanging()
{
if ((this.PropertyChanging != null))
{
this.PropertyChanging(this, emptyChangingEventArgs);
}
}
protected virtual void SendPropertyChanged(String propertyName)
{
if ((this.PropertyChanged != null))
{
this.PropertyChanged(this, new PropertyChangedEventArgs(propertyName));
}
}
}
The ListView XAML Code
<ListView ItemsSource="{Binding ModelCollection,Mode=TwoWay}" SelectedItem="{Binding SelectedModelItem}" Style="{StaticResource viewinglist}">
<ListView.View>
<GridView>
<GridViewColumn DisplayMemberBinding="{Binding Model_No, Mode=TwoWay}" Header="Model No." Width="100"/>
<GridViewColumn DisplayMemberBinding="{Binding Name, Mode=TwoWay}" Header="Model Name" Width="200"/>
<GridViewColumn DisplayMemberBinding="{Binding Manufacturer, Mode=TwoWay}" Header="Manufacturer" Width="200"/>
</GridView>
</ListView.View>
</ListView>
Solution for anyone who sees the post:
This is where i was doing wrong-:
public tbl_Model SelectedModelItem {get; set;}
//on clicking edit this is what i used to do
ModelItem.ID = SelectedModelItem.ID;
ModelItem.Model_No = SelectedModelItem.Model_No;
ModelItem.Name = SelectedModelItem.Name;
ModelItem.Manufacturer = SelectedModelItem.Manufacturer;
The right way :
private tbl_Model _selectedModelItem;
public tbl_Model SelectedModelItem
{
get { return _selectedModelItem; }
set
{
_selectedModelItem = value;
NotifyPropertyChanged("SelectedModelItem");
}
}
on clicking edit
ModelItem = SelectedModelItem;
are you really sure that you update a ModelItem from WITHIN your Collection? you did not post the code.
The bindings and INotifyPropertyChanged implementation looks well.
<ListView ItemsSource="{Binding ModelCollection,Mode=TwoWay}"
SelectedItem="{Binding SelectedModelItem}" />
private tbl_Model _modelItem;
public tbl_Model SelectedModelItem
{
get { return _modelItem; }
private set
{
_modelItem = value;
NotifyPropertyChanged("SelectedModelItem");
}
}
public void Update()
{
SelectedModelItem.Model_No = "102";//Ui get notified, cause its a ModelItem from your Collection
}
ps: and pls remove the TwoWay Binding from
<ListView ItemsSource="{Binding ModelCollection,Mode=TwoWay}"
your ListeView will never set a ModelCollection back to your Viewmodel.
The properties of ModelItem also needs to implement INotifyPropertyChanged.
You have to implement INotifyPropertyChanged in your model.
In case your model is auto generated, then you can create a ViewModel for your model which implements INotifyPropertyChanged.
Each property of your Model or ViewModel needs to yield property changed.
The ObservableCollection raises events automatically but for ModelItem's properties you have to raise the events yourself.
public class ModelItem : INotifyPropertyChanged
{
private int modelNumber;
public int ModelNumber
{
get { return modelNumber; }
set
{
modelNumber = value;
NotifyPropertyChanged("ModelNumber"); }
}
//Similar implementation for other Properties Model Name, Manufacturer
public event PropertyChangedEventHandler PropertyChanged;
private void NotifyPropertyChanged(String propertyName)
{
PropertyChangedEventHandler handler = PropertyChanged;
if (null != handler)
{
handler(this, new PropertyChangedEventArgs(propertyName));
}
}
}

WPF Databind control in write-only mode

I'm trying to fiddle a little with WPF bindings, so I created a simple project.
Here's the code:
public class Person : INotifyPropertyChanged
{
public event PropertyChangedEventHandler PropertyChanged;
public int Age {
get { return age; }
set {
age = value;
FirePropertyChanged("Age");
}
}
public string Name
{
get { return name; }
set
{
name = value;
FirePropertyChanged("Name");
}
}
private void FirePropertyChanged(string v)
{
if(PropertyChanged !=null)
PropertyChanged(this, new PropertyChangedEventArgs(v));
}
private int age;
private string name;
}
My viewmodel contains ObservableCollection of Person, and single Person to track selected Person.
I've bound listbox's ItemsSource to ObservableCollection, and SelectedItem to single Person, called CurrentPerson. Also, I've bound TextBox to CurrentPerson.Name.
Code works fine, but whenever I change content of TextBox - my listbox also changes. And no matter what combination of "OneWay, TwoWay, OneWayToSource" binding modes on listbox\selecteditem I cannot prevent listbox from updating from CurrentPerson.
How can I prevent this behavior? I'd like to update listbox from CurrentPerson only by using ICommand interface from VM.
There is only one copy of the Person object which is being used in both ListBox.ItemsSource and TextBox.Text, so naturally updating that object from one location will reflect the change in the other as well.
Two easy solutions would be
Change the BindingMode on TextBox.Text to Explicit, so it doesn't update the Person object until you tell it to
Use a separate string property for TextBox.Text and copy it over to your SelectedPerson.Name whenever the command executes
Personally I prefer the second option because I'm not a big fan of bindings that don't accurately reflect the data object behind the UI component, and it would allow the user to change the SelectedItem without resetting the TextBox value.
For an example of the second option, your ViewModel might look like this :
public class MyViewModel()
{
ObservableCollection<Person> People { get; set; }
Person SelectedPerson { get; set; }
string NewPersonName { get; set; }
ICommand UpdatePersonName { get; }
}
where the UpdatePersonName command would execute
SelectedPerson.Name = NewPersonName;
and the CanExecute would only return true if
SelectedPerson != null
&& !NewPersonName.IsNullOrWhiteSpace()
&& NewPersonName != SelectedPerson.Name
I'm not sure if I've followed the question properly.
So, we have a class Person as
public class Person : INotifyPropertyChanged
{
public event PropertyChangedEventHandler PropertyChanged;
public int Age
{
get { return age; }
set
{
age = value;
FirePropertyChanged("Age");
}
}
public string Name
{
get { return name; }
set
{
name = value;
FirePropertyChanged("Name");
}
}
private void FirePropertyChanged(string v)
{
if (PropertyChanged != null)
PropertyChanged(this, new PropertyChangedEventArgs(v));
}
private int age;
private string name;
}
And we have a view model as
public class ViewModel : INotifyPropertyChanged
{
public ObservableCollection<Person> List { get; set; }
Person currentPerson;
public Person CurrentPerson {
get { return currentPerson; }
set { currentPerson = value;
FirePropertyChanged("CurrentPerson");
}
}
private void FirePropertyChanged(string v)
{
if (PropertyChanged != null)
PropertyChanged(this, new PropertyChangedEventArgs(v));
}
public event PropertyChangedEventHandler PropertyChanged;
}
The xaml is
<ListBox ItemsSource="{Binding List}" SelectedItem="{Binding CurrentPerson}">
<ListBox.ItemTemplate>
<DataTemplate>
<TextBox Text="{Binding Name}" Width="100" />
</DataTemplate>
</ListBox.ItemTemplate>
</ListBox>
And I bind the view model to the view via
ViewModel vm = new ViewModel();
vm.List = new ObservableCollection<Person>();
foreach (var i in Enumerable.Range(1,10))
{
vm.List.Add(new Person() { Name = "Test" + i.ToString(), Age= i });
}
vm.CurrentPerson = null;
this.DataContext = vm;
Whenever I change the value at textbox, it updates the name properly. I tried to add a handler for list changed, but it doesn't happen to get triggered.
vm.List.CollectionChanged += List_CollectionChanged;
void List_CollectionChanged(object sender, System.Collections.Specialized.NotifyCollectionChangedEventArgs e)
{
MessageBox.Show(e.Action.ToString());
}
Can you comment if it isn't the same as your problem statement?
If you want to control when and what is saved/updated, you obviously need is a ViewModel for editing your Person model.
When selecting a person in your Listbox, you have to pass the person's id (avoid passing the object itself) to the PersonEditViewModel which is bound to the properties that shall be edited, load the persons data into the PersonEditViewModel and then edit. Once you hit the "Save" button, it should commit the change and update the database or whatever you are using for persistence.
Use either events/messages to pass values/events back and forth, or use a navigation approach (like INavigationAware interface in Prism).

MVVM - implementing 'IsDirty' functionality to a ModelView in order to save data

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.

Categories

Resources