So I have the following XAML:
<TextBlock Text="{Binding DisconnectedDevices, UpdateSourceTrigger=PropertyChanged}" />
The view model has the following properties:
public string DisconnectedDevices {get; set;}
public IEnumerable<IDeviceInformationVM> DeviceCollection {get; set;}
There's a method that gets called which raises the property notified event:
public void DeviceCollectionChanged()
{
RaisePropertyChanged(() => DeviceCollection);
}
I'd like to update the value in the TextBlock when DeviceCollection changes. I'm aware that I could just call RaisePropertyChanged on DisconnectedDevices but I'm wondering if its possible to update a TextBlock on a different property change event.
Thanks all!
EDIT: Thanks for the suggestions for using an ObservableCollection instead of IEnumerable, unfortunately, I'm not at liberty to change the collection type..
The DeviceCollectionChanged method is called whenever the collection changes (tedious I know...)
FURTHER EDIT:
Have just gone ahead with
RaisePropertyChanged(() => DisconnectedDevices);
I appreciate not enough information might have been provided in the question to get what I was trying to do, apologies for that
I am not sure if your current code works, but assuming it works.
Why not use - ObservableCollection<IDeviceInformationVM> instead of IEnumerable<IDeviceInformationVM> DeviceCollection you wont need the DeviceCollectionChanged event. It will be taken care.
Yes you can raise
public void DeviceCollectionChanged()
{
RaisePropertyChanged(() => DeviceCollection);
RaisePropertyChanged(() => DisconnectedDevices);
// or RaisePropertyChanged("DisconnectedDevices"); Whichever works
}
See this question, it might help you with implementation of NotifyPropertyChanged for multiple properties- WPF Notify PropertyChanged for a Get Property
Do you call the DeviceCollectionChanged() method everytime you change your DeviceCollection? How do you set DeviceCollection?
You can implement an ObservableCollection (bottom of this answer), or, depending on how you set your DeviceCollection, if for example DeviceCollection comes from a list, you can implement something like this:
private IEnumerable<IDeviceInformationVM> deviceCollection;
public IEnumerable<IDeviceInformationVM> DeviceCollection
{
get
{
return deviceCollection;
}
set
{
deviceCollection = value;
RaisePropertyChanged(() => DisconnectedDevices);
RaisePropertyChanged(() => DeviceCollection);
}
}
DeviceCollection = GetListOfIDeviceInformationVM(); //will automatically raise property changed and update your TextBlock
You won't have to keep on calling RaisePropertyChanged() which looks rather tedious
Change the type of the CollectionDevice collection to ObservableCollection then, raise the event CollectionChanged as follows :
DeviceCollection.CollectionChanged + = DeviceCollection_CollectionChanged;
I give you an implemention in MVVM with a class RelayCommand
here the view : (MainView)
<Window x:Class="WpfApplication.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:local="clr-namespace:WpfApplication"
mc:Ignorable="d"
Title="MainWindow" Height="350" Width="525">
<Window.DataContext>
<local:MainViewModel/>
</Window.DataContext>
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="*"/>
<RowDefinition Height="*"/>
</Grid.RowDefinitions>
<TextBlock Text="{Binding DisconnectedDevices, Mode=TwoWay}" Height="25" Width="175" Grid.Row="0" />
<Button Grid.Row="1" Content="Click" Command="{Binding ToggleExecuteCommand}" Width="100" Height="25"/>
</Grid>
the ViewModel (Main ViewModel)
using System;
using System.Collections.ObjectModel;
using System.Collections.Specialized;
using System.ComponentModel;
namespace WpfApplication
{
public class MainViewModel : INotifyPropertyChanged
{
private string disconnectedDevices;
public event PropertyChangedEventHandler PropertyChanged;
private void NotifyPropertyChanged(String info)
{
if (PropertyChanged != null)
{
PropertyChanged(this, new PropertyChangedEventArgs(info));
}
}
public MainViewModel()
{
ToggleExecuteCommand = new RelayCommand(ChangeCollection);
DeviceCollection = new ObservableCollection<DeviceInformationVM>();
DeviceCollection.CollectionChanged += DeviceCollection_CollectionChanged;
}
private void ChangeCollection(object obj)
{
DeviceCollection.Add(new DeviceInformationVM { MyProperty = "TEST" });
}
private void DeviceCollection_CollectionChanged(object sender, NotifyCollectionChangedEventArgs e)
{
NotifyCollectionChangedAction action = e.Action;
if (action == NotifyCollectionChangedAction.Add)
{
DisconnectedDevices = "Somme thing added to collection";
}
if (action == NotifyCollectionChangedAction.Remove)
{
DisconnectedDevices = "Somme thing removed from collection";
}
}
public string DisconnectedDevices
{
get { return this.disconnectedDevices; }
set
{
if (value != this.disconnectedDevices)
{
this.disconnectedDevices = value;
NotifyPropertyChanged("DisconnectedDevices");
}
}
}
public ObservableCollection<DeviceInformationVM> DeviceCollection { get; set; }
public RelayCommand ToggleExecuteCommand { get; set; }
}
}
the RelayCommand :
using System;
using System.Windows.Input;
namespace WpfApplication
{
public class RelayCommand : ICommand
{
private Action execute;
private Predicate<object> canExecute;
private event EventHandler CanExecuteChangedInternal;
public RelayCommand(Action<object> execute)
: this(execute, DefaultCanExecute)
{
}
public RelayCommand(Action<object> execute, Predicate<object> canExecute)
{
if (execute == null)
{
throw new ArgumentNullException("execute");
}
if (canExecute == null)
{
throw new ArgumentNullException("canExecute");
}
this.execute = execute;
this.canExecute = canExecute;
}
public event EventHandler CanExecuteChanged
{
add
{
CommandManager.RequerySuggested += value;
this.CanExecuteChangedInternal += value;
}
remove
{
CommandManager.RequerySuggested -= value;
this.CanExecuteChangedInternal -= value;
}
}
public bool CanExecute(object parameter)
{
return this.canExecute != null && this.canExecute(parameter);
}
public void Execute(object parameter)
{
this.execute(parameter);
}
public void OnCanExecuteChanged()
{
EventHandler handler = this.CanExecuteChangedInternal;
if (handler != null)
{
handler.Invoke(this, EventArgs.Empty);
}
}
public void Destroy()
{
this.canExecute = _ => false;
this.execute = _ => { return; };
}
private static bool DefaultCanExecute(object parameter)
{
return true;
}
}
}
and finaly DeviceInformation
using System;
namespace WpfApplication
{
public interface IDeviceInformationVM
{
string MyProperty { get; set; }
}
public class DeviceInformationVM : IDeviceInformationVM
{
public string MyProperty
{
get; set;
}
}
}
Hope it helps
Related
I have a listview and a button in each column. When the user clicks a button, it triggers an asynchronous action in the viewmodel where I disable all buttons and do a big action. Once the action is completed, I re-enable them.
If the action takes too long, though, the buttons don't automatically get re-enabled, even though I'm setting the bound property to true and am notifying the view. If the user does ANY GUI action after the action is complete, the buttons will re-enable.
The other weird thing: If I do an await Task.Delay instead of doing Thread.Sleep (NB: I'm doing real work in the full application), it works correctly.
What's going on here?
I've simplified the code here by eliminating the model (all logic lives in the VM).
View-model code:
namespace WpfTestApp
{
public class viewmodel : INotifyPropertyChanged
{
public event PropertyChangedEventHandler PropertyChanged;
public void NotifyPropertyChanged([CallerMemberName] String propertyName = "")
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
public RelayAsyncCommand<object> RunCommand { get; private set; }
private ObservableCollection<subVM> _subVMs;
public ObservableCollection<subVM> SubVMs
{
get => _subVMs; set
{
_subVMs = value;
NotifyPropertyChanged();
}
}
public viewmodel()
{
RunCommand = new RelayAsyncCommand<object>(OnRun);
SubVMs = new ObservableCollection<subVM>
{
new subVM("ItemA"),
new subVM("ItemB"),
};
}
private async void OnRun(object o)
{
subVM vm = o as subVM;
if (vm != null)
{
ChangeRunMode(false);
Thread.Sleep(500);
}
ChangeRunMode(true);
}
private void ChangeRunMode(bool on)
{
foreach (subVM vm in SubVMs)
{
vm.ButtonEnabled = on;
}
}
}
public class subVM : INotifyPropertyChanged
{
private string name = "";
public string Name
{
get => name;
set
{
if (value != name)
{
name = value;
}
}
}
public subVM(string name)
{
Name = name;
}
private bool tsk = true;
public bool ButtonEnabled
{
get => tsk;
set
{
if (tsk != value)
{
tsk = value;
NotifyPropertyChanged("ButtonEnabled");
}
}
}
public event PropertyChangedEventHandler PropertyChanged;
public void NotifyPropertyChanged([CallerMemberName] String propertyName = "")
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
}
}
View XAML:
<Window x:Class="WpfTestApp.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:local="clr-namespace:WpfTestApp"
mc:Ignorable="d"
Title="MainWindow" Height="130" Width="350">
<Window.DataContext>
<local:viewmodel/>
</Window.DataContext>
<ListView Margin="5"
BorderBrush="DarkSlateGray" BorderThickness="1"
ScrollViewer.HorizontalScrollBarVisibility="Disabled"
ItemsSource="{Binding SubVMs}">
<ListView.View>
<GridView>
<GridViewColumn Header="Name"
Width="200" DisplayMemberBinding ="{Binding Name}"/>
<GridViewColumn>
<GridViewColumn.CellTemplate>
<DataTemplate>
<Button
Content="Load"
IsEnabled="{Binding ButtonEnabled, Mode=OneWay, UpdateSourceTrigger=PropertyChanged}"
Margin="0"
VerticalAlignment="Center"
Command="{Binding Path=DataContext.RunCommand, IsAsync=True, RelativeSource={RelativeSource FindAncestor, AncestorType={x:Type ItemsControl}}}"
CommandParameter="{Binding}"
/>
</DataTemplate>
</GridViewColumn.CellTemplate>
</GridViewColumn>
</GridView>
</ListView.View>
</ListView>
</Window>
Things I've tried:
I tried await Task.Run on my actual problem, and it still happens.
RelayCommand and the Async version (I thought these were standard boilerplate, but here you go):
public class RelayAsyncCommand<T> : RelayCommand<T>
{
private bool isExecuting = false;
public event EventHandler Started;
public event EventHandler Ended;
public bool IsExecuting
{
get { return this.isExecuting; }
}
public RelayAsyncCommand(Action<T> execute, Predicate<T> canExecute)
: base(execute, canExecute)
{
}
public RelayAsyncCommand(Action<T> execute)
: base(execute)
{
}
public override Boolean CanExecute(Object parameter)
{
return ((base.CanExecute(parameter)) && (!this.isExecuting));
}
public override void Execute(object parameter)
{
try
{
this.isExecuting = true;
if (this.Started != null)
{
this.Started(this, EventArgs.Empty);
}
Task task = Task.Factory.StartNew(() =>
{
this._execute((T)parameter);
});
task.ContinueWith(t =>
{
this.OnRunWorkerCompleted(EventArgs.Empty);
}, TaskScheduler.FromCurrentSynchronizationContext());
}
catch (Exception ex)
{
this.OnRunWorkerCompleted(new RunWorkerCompletedEventArgs(null, ex, true));
}
}
private void OnRunWorkerCompleted(EventArgs e)
{
this.isExecuting = false;
if (this.Ended != null)
{
this.Ended(this, e);
}
}
}
public class RelayCommand<T> : ICommand
{
#region Fields
readonly protected Action<T> _execute;
readonly protected Predicate<T> _canExecute;
#endregion // Fields
#region Constructors
public RelayCommand(Action<T> execute)
: this(execute, null)
{
}
public RelayCommand(Action<T> execute, Predicate<T> canExecute)
{
if (execute == null)
throw new ArgumentNullException("execute");
_execute = execute;
_canExecute = canExecute;
}
#endregion // Constructors
#region ICommand Members
[DebuggerStepThrough]
public virtual bool CanExecute(object parameter)
{
return _canExecute == null ? true : _canExecute((T)parameter);
}
public event EventHandler CanExecuteChanged
{
add { CommandManager.RequerySuggested += value; }
remove { CommandManager.RequerySuggested -= value; }
}
public virtual void Execute(object parameter)
{
_execute((T)parameter);
}
#endregion // ICommand Members
}
inside the OnRun method you are actually locking the UI thread, preventing any UI update/refresh; you should await for the long operation (and that's exactly what you noticed doing await Task.Delay):
private async void OnRun(object o)
{
subVM vm = o as subVM;
if (vm != null)
{
ChangeRunMode(false);
await Task.Run(() =>
{
//put here your long operation as per your example
for (int i = 0; i < 500; i++)
{
for (int k = 0; k < 100000; k++) ;
}
});
}
ChangeRunMode(true);
}
indeed if you take a closer look to your window, you'll see that currently everything is freezed during the long operation execution, not just only the buttons being disabled.
I figured out my problem:
The button deactivating isn't coming from my binding when I'm running asynchronously. It's coming from the RelayAsyncCommand's CanExecute. CanExecute here returns false while the task is running, but we don't trigger a requery ever when it's done.
Easily fixed by adding a private set to the IsExecuting property, which calls the invalidate/requery function on change (just like the standard notifypropertychanged pattern). For posterity, here's the full fixed RelayAsyncCommand:
public class RelayAsyncCommand<T> : RelayCommand<T>
{
private bool _isExecuting = false;
public event EventHandler Started;
public event EventHandler Ended;
public bool IsExecuting
{
get { return _isExecuting; }
private set
{
if (value != _isExecuting)
{
_isExecuting = value;
CommandManager.InvalidateRequerySuggested();
}
}
}
public RelayAsyncCommand(Action<T> execute, Predicate<T> canExecute)
: base(execute, canExecute)
{
}
public RelayAsyncCommand(Action<T> execute)
: base(execute)
{
}
public override bool CanExecute(object parameter)
{
return ((base.CanExecute(parameter)) && (!IsExecuting));
}
public override void Execute(object parameter)
{
try
{
IsExecuting = true;
Started?.Invoke(this, EventArgs.Empty);
Task task = Task.Factory.StartNew(() =>
{
_execute((T)parameter);
});
task.ContinueWith(t =>
{
OnRunWorkerCompleted(EventArgs.Empty);
}, TaskScheduler.FromCurrentSynchronizationContext());
}
catch (Exception ex)
{
OnRunWorkerCompleted(new RunWorkerCompletedEventArgs(null, ex, true));
}
}
private void OnRunWorkerCompleted(EventArgs e)
{
IsExecuting = false;
Ended?.Invoke(this, e);
}
}
Thanks Peter for making me actually look at (what I thought was) boilerplate code.
What I'm trying to do is: when the textbox contains the value "123" it should enable the button and allow me to click it.
Solution Image &
View Image
I can not find a way to trigger the Button Command (Class called SpecialCommand.cs) based on my Button parameters. Could you support where I'm getting this MVVM pattern wrong?
WPF View [MainWindow.xaml]:
<Window.Resources>
<ViewModel:MainWindowVM x:Key="WindowVm"></ViewModel:MainWindowVM>
</Window.Resources>
<Grid>
<StackPanel>
<TextBox x:Name="textBox" Margin="0, 5" Text="123"/>
<Button Content="Click me!" Margin="0, 5" Command="{Binding SpecialCommand, Source={StaticResource WindowVm}}" CommandParameter="{Binding Text, ElementName=textBox, UpdateSourceTrigger=PropertyChanged, Mode=TwoWay}"/>
</StackPanel>
</Grid>
ViewModel [MainWindowVM.cs]:
public class MainWindowVM
{
private SpecialCommand _specialCommand;
public SpecialCommand SpecialCommand { get => _specialCommand; set => _specialCommand = value; }
public MainWindowVM()
{
_specialCommand = new SpecialCommand();
}
}
Command [SpecialCommand.cs]
public class SpecialCommand : ICommand
{
public bool CanExecute(object parameter)
{
if (parameter != null && (parameter as string) == "123")
return true;
return false;
}
public void Execute(object parameter)
{
MessageBox.Show("Button Pressed!");
}
public event EventHandler CanExecuteChanged;
}
What I believe, maybe this is what I'm getting wrong is since Button & Textbox are in the View I don't need add/modify any method in my SpecialCommand implementation. They should be able to see when a property is changed.
Like the CanExecuteChanged() below, this command raises a lot of times and seems overkill for this small task.
public event EventHandler CanExecuteChanged
{
add { CommandManager.RequerySuggested += value; }
remove { CommandManager.RequerySuggested -= value; }
}
There is no need to make it more complicated than it needs to be.
public class MainWindowVM
{
private string m_OneTwoThree;
public string OneTwoThree{
get { return OneTwoThree; }
set {
if (m_OneTwoThree != value){
m_OneTwoThree = value;
NotifyPropertyChanged(nameof(OneTwoThree)); //if you need this let me know
}
}
}
public MainWindowVM()
{
}
public ICommand RandomCommand { get { return new RelayCommand(OnRandom, IsOneTwoThree); } }
private void OnRandom()
{
//do stuff
}
private bool IsOneTwoThree(){
if (OneTwoThree == "123"){
return true;
}
return false;
}
}
You'll also have to update your xaml I don't think it'll be able to find 'OneTwoThree' so you'll have to bind it yourself, however you usually would.
<StackPanel>
<TextBox x:Name="textBox" Margin="0, 5" Text="{Binding OneTwoThree}"/>
<Button Content="Click me!" Margin="0, 5" Command="{Binding RandomCommand, Source={StaticResource WindowVm}}"/>
</StackPanel>
If you have any questions just ask.
This is my RelayCommand : Use RelayCommand("thing to execute", "if function returns true you can execute");
public class RelayCommand : ICommand
{
public event EventHandler CanExecuteChanged
{
add { CommandManager.RequerySuggested += value; }
remove { CommandManager.RequerySuggested -= value; }
}
private Action methodToExecute;
private Func<bool> canExecuteEvaluator;
public RelayCommand(Action methodToExecute, Func<bool> canExecuteEvaluator)
{
this.methodToExecute = methodToExecute;
this.canExecuteEvaluator = canExecuteEvaluator;
}
public RelayCommand(Action methodToExecute)
: this(methodToExecute, null)
{
}
public bool CanExecute(object parameter)
{
if (this.canExecuteEvaluator == null) {
return true;
}
else {
bool result = this.canExecuteEvaluator.Invoke();
return result;
}
}
public void Execute(object parameter)
{
this.methodToExecute.Invoke();
}
}
You need a way to tell the command that it should check whether it CanExecute. This is done by triggering the event CanExecuteChanged. It tells the button to requery the CanExecute property.
For this to work, I would add a text property to your view model and bind the textbox to it.
In SpecialCommand add the method:
public void TriggerCanExecuteChanged()
{
CanExecuteChanged?.Invoke(this, EventArgs.Empty);
}
In the class MainWindowVM add the property:
private string _text;
public string Text
{
get { return _text; }
set {
if (value != _text) {
_text = value;
_specialCommand.TriggerCanExecuteChanged();
}
}
}
View Model to implementing INotifyPropertyChanged (see comments):
public class MainWindowVM : INotifyPropertyChanged
{
public SpecialCommand SpecialCommand { get; set; } = new SpecialCommand();
private string _text;
public string Text
{
get { return _text; }
set {
if (value != _text) {
_text = value;
OnPropertyChanged(nameof(Text));
SpecialCommand.TriggerCanExecuteChanged();
}
}
}
public event PropertyChangedEventHandler PropertyChanged;
private void OnPropertyChanged(string propertyName)
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
}
Question
How can I make it so that changes to a note are only propagated back to the list, when the Save button is clicked instead on "lost focus"?
And the Save button should only be enabled when the note has been changed.
UI
The example application looks like this:
The current behaviour is:
Clicking on a note puts its text into the TextBox; that's fine.
The changed text from the TextBox gets written back to the list when the TextBox loses the focus (default binding behaviour); but I only want that to happend when the Save button is clicked.
The Save button is always activated because the CanExecute(object parameter) isn't correctly implemented yet; it should only get activated when the TextBox text is different from the selected note's text.
My research so far
Option 1: Some Internet sources say to bind a different property to the TextBox and to programmatically check whether it is different from the SelectedItem of the ListView. I would have hoped that there was a way without introducing a third property in addition to the already existing ListOfNotes and SelectedNote.
Option 2: Some Internet sources recommend to configure Mode=OneWay so that clicking an item in the ListView updates the TextBox, but not the other way around. This sounds like the solution I would prefer, but I wasn't able to figure out from the code examples how to raise an event programmatically so that the change in the TextBox gets written back to the ListView when the Save button is clicked.
I've found other Stackoverflow questions that seem to be similar to mine, but the answers to those haven't helped me fix the problem:
WPF databinding after Save button click
Code
This example currently does two-way binding on focus lost. How do I need to change it to get the above described behaviour?
https://github.com/lernkurve/WpfBindingOneWayWithSaveButton
MainWindow.xaml
<Window x:Class="WpfBindingOneWayWithSaveButton.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:wpfBindingOneWayWithSaveButton="clr-namespace:WpfBindingOneWayWithSaveButton"
mc:Ignorable="d"
Title="MainWindow" Height="188.636" Width="299.242">
<Window.DataContext>
<wpfBindingOneWayWithSaveButton:MainWindowsViewModel />
</Window.DataContext>
<Grid>
<GroupBox Header="List of notes" HorizontalAlignment="Left" VerticalAlignment="Top" Height="112" Width="129" Margin="0,24,0,0">
<ListView ItemsSource="{Binding ListOfNotes}" SelectedItem="{Binding SelectedNote}" DisplayMemberPath="Text" HorizontalAlignment="Left" Height="79" VerticalAlignment="Top" Width="119" Margin="0,10,-2,0"/>
</GroupBox>
<GroupBox Header="Change selected note" HorizontalAlignment="Left" Margin="134,24,0,0" VerticalAlignment="Top" Height="112" Width="151">
<Grid HorizontalAlignment="Left" Height="89" Margin="0,0,-2,0" VerticalAlignment="Top" Width="141">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="40*"/>
<ColumnDefinition Width="101*"/>
</Grid.ColumnDefinitions>
<TextBox Text="{Binding SelectedNote.Text}" HorizontalAlignment="Left" Height="23" TextWrapping="Wrap" VerticalAlignment="Top" Width="121" Margin="10,7,0,0" Grid.ColumnSpan="2"/>
<Button Command="{Binding SaveCommand}" Content="Save" HorizontalAlignment="Left" VerticalAlignment="Top" Width="121" Margin="10,35,0,0" Grid.ColumnSpan="2"/>
</Grid>
</GroupBox>
</Grid>
</Window>
MainWindowsViewModel.cs
using System.Collections.ObjectModel;
using System.Windows.Input;
namespace WpfBindingOneWayWithSaveButton
{
public class MainWindowsViewModel
{
public ObservableCollection<Note> ListOfNotes { get; set; }
public Note SelectedNote { get; set; }
public ICommand SaveCommand { get; set; }
public MainWindowsViewModel()
{
ListOfNotes = new ObservableCollection<Note>
{
new Note { Text = "Note 1" },
new Note { Text = "Note 2" }
};
SaveCommand = new SaveCommand(this);
}
}
}
SaveCommand.cs
using System;
using System.Windows.Input;
namespace WpfBindingOneWayWithSaveButton
{
public class SaveCommand : ICommand
{
private MainWindowsViewModel vm;
public SaveCommand(MainWindowsViewModel vm)
{
this.vm = vm;
}
public bool CanExecute(object parameter)
{
// What should go here?
return true;
// Pseudo code
// return (is the TextBox text different from the original note text)
}
public void Execute(object parameter)
{
// What should go here?
// Pseudo code
// Let WPF know that the TextBox text has changed
// Invoke the binding so it propagates the TextBox text back to the list
}
public event EventHandler CanExecuteChanged;
}
}
Note.cs
using System.ComponentModel;
using System.Runtime.CompilerServices;
namespace WpfBindingOneWayWithSaveButton
{
public class Note : INotifyPropertyChanged
{
private string text;
public string Text
{
get { return text; }
set
{
text = value;
OnPropertyChanged();
}
}
public event PropertyChangedEventHandler PropertyChanged;
protected virtual void OnPropertyChanged([CallerMemberName] string propertyName = null)
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
}
}
Bind the text to the CommandParameter of the SaveButton so it gets passed to the Save method for updating.
<TextBox x:Name="NoteTextBox" Text="{Binding SelectedNote.Text, Mode=OneTime}" ../>
<Button Command="{Binding SaveCommand}"
CommandParameter="{Binding ElementName=NoteTextBox, Path=Text}",
Content="Save" />
and
public bool CanExecute(object parameter)
{
return vm.SelectedNote.Text != parameter as string;
}
public void Execute(object parameter)
{
vm.SelectedNote.Text = parameter as string;
}
Option one is the easiest to implement, you will need to clone the Note object and set it to a separate property.
in your xaml, change your list view to the following so it now binds the SelectedIndex instead of the SelectedItem.
<ListView ItemsSource="{Binding ListOfNotes}" SelectedIndex="{Binding SelectedIndex}" DisplayMemberPath="Text" ...
And change TextBox to the following so it updates the binding as you type
<TextBox Text="{Binding Path=SelectedNote.Text, UpdateSourceTrigger=PropertyChanged}" HorizontalAlignment="Left" ...
In Note.cs we add the Clone() method.
public class Note : INotifyPropertyChanged
{
public Note Clone()
{
return new Note()
{
Text = this.Text
};
}
//... The rest stays the same
}
In MainWindowsViewModel.cs we add new properties for the SelectedIndex and clone the object when we detect a index has changed. We also need to add INotifyPropertyChanged so we can update the SelectedNote from the codebehind when we do the Clone()
public class MainWindowsViewModel : INotifyPropertyChanged
{
private int _selectedIndex = -1;
private Note _selectedNote;
public int SelectedIndex
{
get { return _selectedIndex; }
set
{
if (_selectedIndex.Equals(value))
return;
_selectedIndex = value;
CloneSelectedNote();
}
}
private void CloneSelectedNote()
{
if (SelectedIndex >= 0)
{
SelectedNote = ListOfNotes[SelectedIndex].Clone();
}
else
{
SelectedNote = null;
}
}
public event PropertyChangedEventHandler PropertyChanged;
protected virtual void OnPropertyChanged([CallerMemberName] string propertyName = null)
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
public Note SelectedNote
{
get { return _selectedNote; }
set
{
if(Equals(_selectedNote, value))
return;
_selectedNote = value;
OnPropertyChanged();
}
}
//... The rest stays the same
}
In SaveCommand.cs we add the logic for CanExecute and add the subscriptions to CommandManager.RequerySuggested, this automatically makes it requery the CanExecute any time any binding changes. This can be a little ineffecent, if you wanted to you could expose a RaiseCanExecuteChanged() publicly but it would be MainWindowsViewModel responsibility to call it any time vm.SelectedIndex or vm.SelectedNote.Text changed.
public class SaveCommand : ICommand
{
private MainWindowsViewModel vm;
public SaveCommand(MainWindowsViewModel vm)
{
this.vm = vm;
}
public bool CanExecute(object parameter)
{
if (vm.SelectedIndex < 0 || vm.SelectedNote == null)
return false;
return vm.ListOfNotes[vm.SelectedIndex].Text != vm.SelectedNote.Text;
}
public void Execute(object parameter)
{
vm.ListOfNotes[vm.SelectedIndex] = vm.SelectedNote;
}
public event EventHandler CanExecuteChanged
{
add { CommandManager.RequerySuggested += value; }
remove { CommandManager.RequerySuggested -= value; }
}
}
UPDATE: Here is a updated version that does not use CommandManager
MainWindowsViewModel.cs
public class MainWindowsViewModel : INotifyPropertyChanged
{
private int _selectedIndex = -1;
private Note _selectedNote;
public int SelectedIndex
{
get { return _selectedIndex; }
set
{
if (_selectedIndex.Equals(value))
return;
_selectedIndex = value;
CloneSelectedNote();
RecheckSaveCommand();
}
}
private void CloneSelectedNote()
{
if (SelectedIndex >= 0)
{
SelectedNote = ListOfNotes[SelectedIndex].Clone();
}
else
{
SelectedNote = null;
}
}
public event PropertyChangedEventHandler PropertyChanged;
protected virtual void OnPropertyChanged([CallerMemberName] string propertyName = null)
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
public Note SelectedNote
{
get { return _selectedNote; }
set
{
if(Equals(_selectedNote, value))
return;
if (_selectedNote != null)
{
PropertyChangedEventManager.RemoveHandler(_selectedNote, SelectedNoteTextChanged, nameof(Note.Text));
}
_selectedNote = value;
if (_selectedNote != null)
{
PropertyChangedEventManager.AddHandler(_selectedNote, SelectedNoteTextChanged, nameof(Note.Text));
}
OnPropertyChanged();
}
}
private void SelectedNoteTextChanged(object sender, PropertyChangedEventArgs propertyChangedEventArgs)
{
RecheckSaveCommand();
}
private void RecheckSaveCommand()
{
var command = this.SaveCommand as WpfBindingOneWayWithSaveButton.SaveCommand; //"this." and "WpfBindingOneWayWithSaveButton." are not necessary but I wanted to be explicit.
if (command != null)
{
command.RaiseCanExecuteChanged();
}
}
//...
}
SaveCommand.cs
public class SaveCommand : ICommand
{
private MainWindowsViewModel vm;
public SaveCommand(MainWindowsViewModel vm)
{
this.vm = vm;
}
public bool CanExecute(object parameter)
{
if (vm.SelectedIndex < 0 || vm.SelectedNote == null)
return false;
return vm.ListOfNotes[vm.SelectedIndex].Text != vm.SelectedNote.Text;
}
public void Execute(object parameter)
{
vm.ListOfNotes[vm.SelectedIndex] = vm.SelectedNote;
}
public event EventHandler CanExecuteChanged;
public void RaiseCanExecuteChanged()
{
CanExecuteChanged?.Invoke(this, EventArgs.Empty);
}
}
You should not use OneWay but rather an UpdateSourceTrigger of value Explicit. BindingGroups can do this for you though, here's a simple example:
<!-- For change observation -->
<TextBlock Text="{Binding Text}"></TextBlock>
<StackPanel>
<StackPanel.BindingGroup>
<BindingGroup x:Name="EditGroup"></BindingGroup>
</StackPanel.BindingGroup>
<TextBox Text="{Binding Text}"></TextBox>
<Button>
<Button.Command>
<local:CommitGroupCommand BindingGroup="{x:Reference EditGroup}"/>
</Button.Command>
Save
</Button>
</StackPanel>
public class CommitGroupCommand : ICommand
{
public BindingGroup BindingGroup { get; set; }
public event EventHandler CanExecuteChanged;
public bool CanExecute(object parameter)
{
return true;
}
public void Execute(object parameter)
{
BindingGroup.UpdateSources();
}
}
(You could add a validation rule to your binding that requires the value to be different and use that for the CanExecute implementation.)
Using this method allows you to bind directly to the object you intend to edit, so you don't need to copy around values first.
I noticed that it happends not only in the one project but on multiple too so I will provide simple example. I've got such xaml:
<Page
x:Class="TestApp.MainPage"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="using:TestApp"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
mc:Ignorable="d"
Background="{ThemeResource ApplicationPageBackgroundThemeBrush}">
<Grid>
<Button Content="Button" Command="{Binding PressedButton}" HorizontalAlignment="Left" Margin="0,-10,0,-9" VerticalAlignment="Top" Height="659" Width="400"/>
</Grid>
</Page>
my classes to binding data:
public abstract class ObservableObject : INotifyPropertyChanged
{
public event PropertyChangedEventHandler PropertyChanged;
protected virtual void OnPropertyChanged(string propertyName)
{
if (this.PropertyChanged != null)
{
var e = new PropertyChangedEventArgs(propertyName);
this.PropertyChanged(this, e);
}
}
}
public class Command : ICommand
{
private Action<object> action;
public Command(Action<object> action)
{
this.action = action;
}
public bool CanExecute(object parameter)
{
if (action != null)
{
return true;
}
else
{
return false;
}
}
public event EventHandler CanExecuteChanged;
public void Execute(object parameter)
{
if (action != null)
{
action((string)parameter);
}
}
}
public class TestViewModel : ObservableObject
{
public ICommand PressedButton
{
get
{
return new Command((param) => { });
}
}
}
and main page:
public MainPage()
{
this.InitializeComponent();
this.NavigationCacheMode = NavigationCacheMode.Required;
DataContext = new TestViewModel();
}
It's weird but PressedButton runs only on application start(isn't that weird it runs on start?). After that, even after button click there is nothing triggered. I can't figure out what's wrong.
I think you may be causing binding issues by returning a new command each time the "getter" is called. Try setting the command once, in your constructor (for example).
public MainPage()
{
PressedAdd = new Command(param => SaveNote());
}
public ICommand PressedAdd { get; private set; }
In the SaveNote() method, you could test the values and either save (or not save) them:
private void SaveNote()
{
if (NoteTitle == null || NoteContent == null)
return;
// Do something with NoteTitle and NoteContent
}
I am new to the MVVM pattern and things are coming to me ever so slowly, I want to be able to click a button on my form and then it dynamically create a textbox at runtime. I have a 'Add Title' and also 'Add Question' which both add textboxes but at different locations, you can add as many questions under one title. I have Created a class called Standard in this class it holds:
public class Standard
{
string _title;
ObservableCollection<string> _questions;
public event PropertyChangedEventHandler PropertyChanged;
#region NofiftyPropChnage
protected void NotifyOfPropertyChanged(string name)
{
PropertyChangedEventHandler handler = PropertyChanged;
if (handler != null)
{
handler(this, new PropertyChangedEventArgs(name));
}
}
protected void NotifyOfPropertyChanged<TProperty>(Expression<Func<TProperty>> property)
{
NotifyOfPropertyChanged(property.GetMemberInfo().Name);
}
#endregion
#region Properties
public string Title
{
get { return _title; }
set
{
_title = value;
NotifyOfPropertyChanged(() => Title);
}
}
public ObservableCollection<string> Questions
{
get { return _questions; }
set
{
_questions = value;
NotifyOfPropertyChanged(() => Questions);
}
}
#endregion
}
This class holds a Title property and also a list of Questions property because you can add Questions under a Title.
I also have a ViewModel class which holds:
class ViewModel :INotifyPropertyChanged
{
#region NotifyPropertyChange
public event PropertyChangedEventHandler PropertyChanged;
protected void NotifyOfPropertyChanged(string name)
{
PropertyChangedEventHandler handler = PropertyChanged;
if (handler != null)
{
handler(this, new PropertyChangedEventArgs(name));
}
}
protected void NotifyOfPropertyChanged<TProperty>(Expression<Func<TProperty>> property)
{
NotifyOfPropertyChanged(property.GetMemberInfo().Name);
}
#endregion
private ObservableCollection<Standard> _standardCollection;
public ObservableCollection<Standard> StandardCollection
{
get
{
return _standardCollection;
}
set
{
_standardCollection = value;
NotifyOfPropertyChanged(() => StandardCollection);
}
}
}
This class holds a list of standards, a standard is when you click save with the text boxes and information in the text boxes done. It saves as a Standard
Finally my XAML code:
<Grid>
<button Content="Add Title"/>
<button Content="Add Question"/>
<StackPanel>
<ItemsControl ItemsSource="{Binding StandardCollection}">
<ItemsControl.ItemTemplate>
<DataTemplate DataType="{x:Type local:Standard}">
<Grid>
<TextBox Text="{Binding Title}"/>
<ItemsControl ItemsSource="{Binding Questions}">
<ItemsControl.ItemTemplate>
<DataTemplate>
<TextBox Text="{Binding Questions}"/>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
</Grid>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
</StackPanel>
</Grid>
Everything runs and there are no errors but when I click 'Add Title' or 'Add Question' no textbox appears, any help?
Ok, I'll have another shot at this one. I've stripped out the Title part and just concentrated on the Questions in order to keep this as a minimal example. First you'll need a base class that implements INotifyPropertyChanged for your view models:
public abstract class ObservableObject : INotifyPropertyChanged
{
public event PropertyChangedEventHandler PropertyChanged;
protected void OnPropertyChanged<T>(Expression<Func<T>> propertyExpresion)
{
var property = (MemberExpression)propertyExpresion.Body;
this.OnPropertyChanged(property.Member.Name);
}
protected void OnPropertyChanged(string propertyName)
{
if (this.PropertyChanged != null)
{
this.PropertyChanged(this, new PropertyChangedEventArgs(propertyName));
}
}
}
Next you'll need a class that implements ICommand for your buttons to bind to which causes handlers to get called when those buttons are pressed:
// by Josh Smith, http://msdn.microsoft.com/en-us/magazine/dd419663.aspx
public class RelayCommand : ICommand
{
#region Fields
readonly Action<object> _execute;
readonly Predicate<object> _canExecute;
#endregion // Fields
#region Constructors
public RelayCommand(Action<object> execute)
: this(execute, null)
{
}
public RelayCommand(Action<object> execute, Predicate<object> canExecute)
{
if (execute == null)
throw new ArgumentNullException("execute");
_execute = execute;
_canExecute = canExecute;
}
#endregion // Constructors
#region ICommand Members
public bool CanExecute(object parameter)
{
return _canExecute == null ? true : _canExecute(parameter);
}
public event EventHandler CanExecuteChanged
{
add { CommandManager.RequerySuggested += value; }
remove { CommandManager.RequerySuggested -= value; }
}
public void Execute(object parameter)
{
_execute(parameter);
}
#endregion // ICommand Members
}
Those two classes were written by others, if you add MVVM Lite project to your project you'll get them provided for you.
Next we need to create a view model with an ObservableCollection of Questions and a handler that gets called when the user presses the button:
public class MyViewModel : ObservableObject
{
public ICommand AddQuestionCommand {get; private set;}
ObservableCollection<string> _questions = new ObservableCollection<string>();
public ObservableCollection<string> Questions
{
get { return _questions; }
set
{
_questions = value;
OnPropertyChanged(() => Questions);
}
}
public MyViewModel()
{
this.AddQuestionCommand = new RelayCommand(new Action<object>((o) => OnAddQuestion()));
}
private void OnAddQuestion()
{
this.Questions.Add("new item");
}
}
Obviously you'll need to create an instance of this and set it as your window's DataContext. When the command gets triggerd the handler gets called and it in turn adds a new string to the collection. The XAML now needs to bind a button to that command and use the Questions collection to create a list of TextBlocks that display them all:
<StackPanel>
<Button Content="Add Question" Command="{Binding AddQuestionCommand}" HorizontalAlignment="Left"/>
<ItemsControl ItemsSource="{Binding Questions}">
<ItemsControl.ItemTemplate>
<DataTemplate>
<TextBox Text="{Binding .}" Width="200" HorizontalAlignment="Left"/>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
</StackPanel>
Hopefully this should give you a starting point. If I've missed something or you need clarification on anything then pls post a follow-up and I'll do my best.
Standard needs to implement the INotifyPropertyChanged interface. Generally you shouldn't do this more than once though, just declare one base class that implements that stuff and inherit all your view models from that. Also if you use package manager to add MVVM Lite to your project then you'll get a lot of this stuff provided for you.
I have no idea why these other guys are banging on about the INotifyPropertyChanged interface, as that has so very little to do with ICommand, although it does appear that you have tried to use it without adding it to the Standard class definition.
Either way, it sounds to me like you need to use the RelayCommand, or similar. This is a class that extends the ICommand interface... you can think of it as a delegate command. Instead of defining a separate class for each command, you can simply define the command logic and the canExecute handler inline. Here is a simplified example:
public ICommand SaveCommand
{
get { return new RelayCommand(execute => Save(), canExecute => CanSave()); }
}
...
<Button Content="Save" Command="{Binding SaveCommand}" />
You can find an implementation of it in the RelayCommand.cs page on GitHub and a description of it in the Commands, RelayCommands and EventToCommand page on MDSN Magazine.
You will need to change your code heavily to make it work. Do the following:
Step 1. Add Class RelayCommand:
public class RelayCommand : ICommand
{
public Func<bool> CanExecute { get; set; }
public Action Execute { get; set; }
public RelayCommand()
{
}
public RelayCommand(Action execute)
{
Execute = execute;
}
#region ICommand Members
bool ICommand.CanExecute(object parameter)
{
if (this.CanExecute == null)
{
return true;
}
else
{
return this.CanExecute();
}
}
event EventHandler ICommand.CanExecuteChanged
{
add { CommandManager.RequerySuggested += value; }
remove { CommandManager.RequerySuggested -= value; }
}
void ICommand.Execute(object parameter)
{
this.Execute();
}
#endregion
}
Step 2. Add Commands in ViewModel
public ICommand AddTitle { get; private set; }
public ICommand AddQuestion { get; private set; }
public ViewModel()
{
_standardCollection = new ObservableCollection<Standard>();
AddTitle = new RelayCommand(OnAddTitle);
AddQuestion = new RelayCommand(OnAddQuestion);
}
void OnAddTitle()
{
_standardCollection.Add(new Standard());
}
void OnAddQuestion()
{
_standardCollection.Last().Questions.Add(new Question("Some Question"));
}
Step 3. Bind buttons
<Button Content="Add Title" Command="{Binding AddTitle}"/>
<Button Content="Add Question" Command="{Binding AddQuestion}"/>
You will also have to fix you layount in XAML.
Since the user can change the question text, you should create a separate class Question.
Try implementing INotifyPropertyChanged on class Standard.
public class Standard : INotifyPropertyChanged
{
string _title;
ObservableCollection<string> _questions;
public event PropertyChangedEventHandler PropertyChanged;
#region NofiftyPropChnage
protected void NotifyOfPropertyChanged(string name)
{
PropertyChangedEventHandler handler = PropertyChanged;
if (handler != null)
{
handler(this, new PropertyChangedEventArgs(name));
}
}
protected void NotifyOfPropertyChanged<TProperty>(Expression<Func<TProperty>> property)
{
NotifyOfPropertyChanged(property.GetMemberInfo().Name);
}
#endregion
#region Properties
public string Title
{
get { return _title; }
set
{
_title = value;
NotifyOfPropertyChanged(() => Title);
}
}
public ObservableCollection<string> Questions
{
get { return _questions; }
set
{
_questions = value;
NotifyOfPropertyChanged(() => Questions);
}
}
#endregion
}