WPF frame content not updating when source Uri is changed through binding - c#

I've been working on this issue for a few days and can't seem to find anything that will work for my application.
My issue is that I am trying to use a User Control containing buttons to bind to commands which change the source Uri of a frame (both displaying in the same window). When I click a button it is changing the Uri within the ViewModel but the frame does not change the page to reflect this. I believe that it is either not picking up the change due to the way it is binding or there is something blocking it from changing the page which is displaying in the frame.
I am using the MVVM pattern which has been great until I reached the point that I had to start dealing with navigation. Any help would be greatly appreciated!
Navigation User Control View Buttons:
<Button Name="BtnMainDash" Content="Main Dashboard" HorizontalAlignment="Left" Margin="10,10,0,0" VerticalAlignment="Top" Width="180" Command="{Binding MainDashboard}"/>
<Button Name="BtnAccount" Content="Account" HorizontalAlignment="Left" Margin="10,40,0,0" VerticalAlignment="Top" Width="180" Command="{Binding EditAccount}"/>
<Button Name="BtnProjects" Content="Projects" HorizontalAlignment="Left" Margin="10,70,0,0" VerticalAlignment="Top" Width="180" Command="{Binding ProjectScreen}"/>
Main Window Frame:
<Frame x:Name="FmePages" Margin="200,30,-0.4,0.4"
Source="{Binding Path=CurrentPage, Mode=OneWay, UpdateSourceTrigger=PropertyChanged }"
NavigationUIVisibility="Hidden"/>
Button ICommands (All the same except that each calls a difference Uri changing command):
using System;
using System.Windows.Input;
using ScrumManagementApplication.Pages.MainWindow.ViewModel;
namespace ScrumManagementApplication.Pages.MainWindow.Commands
{
class LoadEditAccount : ICommand
{
private readonly NavigationViewModel _navigationViewModel;
public LoadEditAccount(NavigationViewModel navigationViewModel)
{
_navigationViewModel = navigationViewModel;
}
public event EventHandler CanExecuteChanged
{
add { CommandManager.RequerySuggested += value; }
remove { CommandManager.RequerySuggested -= value; }
}
public bool CanExecute(object parameter)
{
return _navigationViewModel.CommandsEnabled;
}
public void Execute(object parameter)
{
_navigationViewModel.LoadEditAccount();
}
}
}
ViewModel:
using System;
using System.ComponentModel;
using System.Windows;
using System.Windows.Forms;
using System.Windows.Input;
using ScrumManagementApplication.Pages.MainWindow.Commands;
using ScrumManagementApplication.SessionData;
using MessageBox = System.Windows.MessageBox;
namespace ScrumManagementApplication.Pages.MainWindow.ViewModel
{
public class NavigationViewModel : INotifyPropertyChanged, INotifyPropertyChanging
{
public bool CommandsEnabled = false;
public NavigationViewModel()
{
MainDashboard = new LoadMainDashboard(this);
EditAccount = new LoadEditAccount(this);
ProjectScreen = new LoadProjectScreen(this);
LogOut = new LoadLogOut(this);
CommandsEnabled = true;
LoadEditAccount();
}
#region ICommands
public ICommand MainDashboard { get; private set; }
public void LoadMainDashboard()
{
_currentPage = null;
_currentPage = new Uri("pack://application:,,,/Pages/MainWindow/View/MainDashboardView.xaml", UriKind.Absolute);
}
public ICommand EditAccount { get; private set; }
public void LoadEditAccount()
{
_currentPage = null;
_currentPage = new Uri("pack://application:,,,/Pages/EditUserDetailsPage/View/EditUserDetailsView.xaml", UriKind.Absolute);
}
public ICommand ProjectScreen { get; private set; }
public void LoadProjectScreen()
{
_currentPage = null;
_currentPage = new Uri("pack://application:,,,/Pages/ProjCreationPage/View/ProjectCreationPage.xaml", UriKind.Absolute);
}
public ICommand LogOut { get; private set; }
public void LoadLogOut()
{
var dialogResult = MessageBox.Show("Are you sure you want to log out?", "Log Out", MessageBoxButton.YesNo);
if (dialogResult == (MessageBoxResult) DialogResult.Yes)
{
App.Current.Shutdown();
}
}
#endregion // ICommands
#region MainFrame
private Uri _currentPage;
public Uri CurrentPage
{
get { return _currentPage; }
set
{
_currentPage = value;
OnPropertyChanged("CurrentPage");
}
}
#endregion // MainFrame
#region INotifyPropertyChanged Members
public event PropertyChangedEventHandler PropertyChanged;
public virtual void OnPropertyChanged(String propertyName)
{
PropertyChangedEventHandler handler = PropertyChanged;
if (handler != null)
{
handler(this, new PropertyChangedEventArgs(propertyName));
}
}
#endregion // INotifyPropertyChanged Members
public event PropertyChangingEventHandler PropertyChanging;
protected void OnPropertyChanging(String propertyName)
{
PropertyChangingEventHandler handler = PropertyChanging;
if (handler != null)
{
handler(this, new PropertyChangingEventArgs(propertyName));
}
}
}
}
Any & all help appreciated, even if it doesn't fully solve my issue anything that helps me get closer to a solution is good!
Thanks in advance

Instead of doing this
_currentPage = //some value;
do this:
CurrentPage = //some value
The event will be raised when you change the property and not the backing field.
EDIT
One more suggestion is to create a single command class since all your commands are setting a string value to a property. You can get the button name using the CommandParameter. Based on that set the Uri.

Got a similar Problem, had a mainwindow with a frame in it and when I, for example, opened an openfiledialog and closed it, this frame (some others too) did not update even if I called PropertyChanged() and the bound Property had the right value. The only solution for me was to remove the binding and handle the Content with NavigationService instead. If you need this Property you bound to for some other approaches you could do something like:
private Page _dataExplorerContent;
public Page DataExplorerContent
{
get { return _dataExplorerContent; }
set
{
if(value != null)
{
contentFrame.Navigate(value)
SetField(ref _dataExplorerContent, value);
}
}
}

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);

UWP MVVM Data Binding for dummies (textbox.text from String)

Well, having a go at MVVM with UWP template 10. I have read many pages, and although everyone tries to say its really easy, I still can't make it work.
To put it into context, OCR is being run on an image, and I would like the text to be displayed in textbox automatically.
Here is my Model:
public class TextProcessing
{
private string _ocrText;
public string OcrText
{
get { return _ocrText; }
set
{
_ocrText = value;
}
}
}
Here is my ViewModel:
public class ScanPageViewModel : ViewModelBase, INotifyPropertyChanged
{
public event PropertyChangedEventHandler PropertyChanged;
private TextProcessing _ocrTextVM;
public ScanPageViewModel()
{
_ocrTextVM = new TextProcessing();
}
public TextProcessing OcrTextVM
{
get { return _ocrTextVM; }
set {
_ocrTextVM = value;
this.OnPropertyChanged("OcrTextVM");
}
}
public void OnPropertyChanged([CallerMemberName] string propertyName = null)
{
this.PropertyChanged(this, new PropertyChangedEventArgs(propertyName));
}
Here is my View:
<TextBox x:Name="rtbOcr"
Text="{Binding OcrTextVM.OcrText, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}"
Firstly, that is not working. Could someone try to show where I am going wrong?
Then, the data is coming from a Services file, how would the Services update the value? What would be the correct code?
Thanks in advance.
Following code is cite from code.msdn (How to achieve MVVM design patterns in UWP), it will be helpful for you:
Check you code step by step.
1.ViewModel implemented interface INotifyPropertyChanged,and in property set method invoked PropertyChanged, like this:
public sealed class MainPageViewModel : INotifyPropertyChanged
{
public event PropertyChangedEventHandler PropertyChanged;
private string _productName;
public string ProductName
{
get { return _productName; }
set
{
_productName = value;
if (PropertyChanged != null)
{
PropertyChanged.Invoke(this, new PropertyChangedEventArgs(nameof(ProductName)));
}
}
}
}
2.Initialize you ViewMode in you page, and set DataContext as the ViewMode, like this:
public sealed partial class MainPage : Page
{
public MainPageViewModel ViewModel { get; set; } = new MainPageViewModel();
public MainPage()
{
...
this.DataContext = ViewModel;
}
}
3.In you xaml, binding data from viewMode, like this:
<TextBox Text="{Binding Path=ProductName,Mode=TwoWay,UpdateSourceTrigger=PropertyChanged}" Name="ProductNameTextBox" TextChanged="ProductNameTextBox_TextChanged" />
Your OnPropertyChanged call on OcrTextVM isn't actually called in your case, since you set the value in the constructor to its backing field and bypass the property.
If you set the value via the property, it should work:
public ScanPageViewModel()
{
OcrTextVM = new TextProcessing();
}
Of course your view needs to know that ScanPageViewModel is its DataContext. Easiest way to do it is in the constructor of the code-behind of your view:
public OcrView()
{
DataContext = new ScanPageViewModel();
InitializeComponent();
}
Assuming your OCR service is returning a new TextProcessing object on usage, setting the property of OcrTextVM should suffice:
public class ScanPageViewModel : ViewModelBase, INotifyPropertyChanged
{
//...
private void GetOcrFromService()
{
//...
TextProcessing value = OcrService.Get();
OcrTextVM = value;
}
}
On a note, the OcrTextVM name doesn't really reflect what the property is doing, since it doesn't look like it's a viewmodel. Consider renaming it.
Actually, it is very easy once I manage to understand. Here is the code needed to update a TextBox.Text
In the Models:
public class DisplayText : INotifyPropertyChanged
{
public event PropertyChangedEventHandler PropertyChanged;
private string _text;
public string Text
{
get { return _text; }
set
{
_text = value;
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(Text)));
}
}
}
In the XAML file:
<TextBox Text="{Binding Helper.Text, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}" ... />
In the ViewModels:
private DisplayText _helper = new DisplayText();
public DisplayText Helper
{
get { return _helper; }
set
{
_helper = value;
}
}
Then any mod from the ViewModels:
Helper.Text = "Whatever text, or method returning a string";

How to bind textbox object to ViewModel

Trying to make my first application with the simple logging function to the TextBox on main form.
To implement logging, I need to get the TextBox object into the logger's class.
Prob - can't do that :) currently have no error, but as I understand the text value of TextBox is binding to my ViewModel, because getting 'null reference' exception trying to execute.
Logger.cs
public class Logger : TextWriter
{
TextBox textBox = ViewModel.LogBox;
public override void Write(char value)
{
base.Write(value);
textBox.Dispatcher.BeginInvoke(new Action(() =>
{
textBox.AppendText(value.ToString());
}));
}
public override Encoding Encoding
{
get { return System.Text.Encoding.UTF8; }
}
}
ViewModel.cs
public class ViewModel
{
public int ThreadCount { get; set; }
public int ProxyTimeout { get; set; }
public static TextBox LogBox { get; set; }
//private TextBox _LogBox;
//public TextBox LogBox {
// get { return _LogBox; }
// set {
// _LogBox = value;
// }
//}
}
launching on btn click, MainWindow.xaml.cs:
public partial class MainWindow : Window
{
public MainWindow()
{
InitializeComponent();
DataContext = new ViewModel();
}
private void button1_Click(object sender, RoutedEventArgs e)
{
Logger logger = new Logger();
logger.Write("ewgewgweg");
}
}
MainWindow.xaml
<Window
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:local="clr-namespace:tools"
xmlns:xctk="http://schemas.xceed.com/wpf/xaml/toolkit" x:Class="tools.MainWindow"
mc:Ignorable="d"
Title="Tools" Height="399.387" Width="575.46">
<TextBox x:Name="logBox"
ScrollViewer.HorizontalScrollBarVisibility="Auto"
ScrollViewer.VerticalScrollBarVisibility="Auto" HorizontalAlignment="Left" Height="137" Margin="10,222,0,0" TextWrapping="Wrap" VerticalAlignment="Top" Width="394" Text="{Binding Path = LogBox, Mode=TwoWay}"/>
You have several issues in your code:
Don't bring controls (TextBox) in your viewmodel, if you do there's no use in trying to do MVVM.
The Text property in XAML has to be of the type String or something that can be converted to a string. You're binding a control, which will result in showing System.Windows.Controls.TextBox (result of .ToString()) on your screen instead of actual text.
Your LogBox property should implement INotifyPropertyChanged
You don't want TwoWay binding, as the text flows from your logger to the UI, you don't need it to flow back. You might even consider using a TextBlock instead or make the control readonly so people can't change the content.
You don't want static properties or static viewmodels, read up on dependency injection on how to pass dependencies.
You will be flooding your UI thread by appending your characters one by one. Consider using another implementation (but I won't go deeper into this for this answer).
Keeping all above in mind, I transformed your code to this.
MainWindow.xaml
<TextBox x:Name="logBox"
HorizontalAlignment="Left" VerticalAlignment="Top" Height="137" Margin="10,222,0,0"
TextWrapping="Wrap" Width="394" Text="{Binding Path = LogBox}"/>
MainWindow.xaml.cs
public partial class MainWindow : Window
{
private Logger _logger;
public MainWindow()
{
InitializeComponent();
var viewModel = new ViewModel();
DataContext = viewModel;
_logger = new Logger(viewModel); // passing ViewModel through Dependency Injection
}
private void button1_Click(object sender, RoutedEventArgs e)
{
_logger.Write("ewgewgweg");
}
}
ViewModel.cs
public class ViewModel : INotifyPropertyChanged
{
public int ThreadCount { get; set; }
public int ProxyTimeout { get; set; }
private string _logBox;
public string LogBox
{
get { return _logBox; }
set
{
_logBox = value;
OnPropertyChanged();
}
}
public event PropertyChangedEventHandler PropertyChanged;
protected virtual void OnPropertyChanged([CallerMemberName] string propertyName = null)
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
}
Logger.cs
public class Logger : TextWriter
{
private readonly ViewModel _viewModel;
public Logger(ViewModel viewModel)
{
_viewModel = viewModel;
}
public override void Write(char value)
{
base.Write(value);
_viewModel.LogBox += value;
}
public override Encoding Encoding
{
get { return System.Text.Encoding.UTF8; }
}
}
You can use string instead of TextBox as follow as
In view model class
public class ViewModel : INotifyPropertyChanged
{
public event PropertyChangedEventHandler PropertyChanged;
private string _logBox;
public string LogBox
{
get {return _logBox;}
set
{
if(value != _logBox)
{
_logBox=value;
OnPropertyChanged("LogBox");
}
}
}
protected void OnPropertyChanged(string name)
{
PropertyChangedEventHandler handler = PropertyChanged;
if (handler != null)
{
handler(this, new PropertyChangedEventArgs(name));
}
}
}
and in writer method you just
public void writer (string str)
{
ViewModel.LogBox = str;
}
You can define ViewModel as static or create new object from ViewModel and access the object in logger class as you want!
hope this helped.

Dynamic Binding is not updating

I'm a beginner in wpf. I am following a textbook to learn, the examples are shown but its not working whenever I am writing dynamic binding part even after following each of its instructions strictly thrice.
this is the code behind
using System;
using System.ComponentModel;
namespace KarliCards_GUI
{
[Serializable]
public class GameOptions:INotifyPropertyChanged
{
private bool _playAgainstComputer = false;
public bool PlayAgainstComputer
{
get
{
return _playAgainstComputer;
}
set
{
_playAgainstComputer = value;
OnPropertyChanged("PlayAgainstComputer");
}
}
public event PropertyChangedEventHandler PropertyChanged;
private void OnPropertyChanged(string propertyName)
{
if (PropertyChanged != null)
PropertyChanged(this, new PropertyChangedEventArgs(propertyName));
}
}
}
In XAML file I want to dynamically bind IsChecked property of a checkbox by using DataContext which will have an instance of GameOptions.
Below part of code is in XAML file
<CheckBox Content="Play against computer" HorizontalAlignment="Left" Margin="11,33,0,0" VerticalAlignment="Top" Name="playAgainstComputerCheck" IsChecked="{Binding Path=PlayAgainstComputer}" />
and below is the code of csharp file of that XAML
namespace KarliCards_GUI
{
public partial class Options : Window
{
private GameOptions _gameOptions;
public Options()
{
if (_gameOptions == null)
{
if (File.Exists("GameOptions.xml"))
{
using (var stream = File.OpenRead("GameOptions.xml"))
{
var serializer = new XmlSerializer(typeof(GameOptions));
_gameOptions = serializer.Deserialize(stream) as GameOptions;
}
}
else
_gameOptions = new GameOptions();
}
DataContext = _gameOptions;
InitializeComponent();
}
}
}
The problem I am facing is, in the property 'PlayAgainstComputer', in set part if I set the variable(_playAgainstComputer) as 'value' it is always checked in the checkbox.
Might be the case the since the InitializeComponent() method is being called after the DataContext is set.
The default value for IsChecked property of CheckBox is true. I suggest first to call the InitializeComponent() method and then set the DataContext.

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