Caliburn.Micro, MVVM pattern: CanExecute command doesn't work - c#

This is my View:
<StackPanel Orientation="Horizontal" VerticalAlignment="Top">
<Label>Customer name:</Label>
<TextBox Text="{Binding Customer.Name, UpdateSourceTrigger=PropertyChanged}" Width="136"/>
<Button x:Name="UpdateClick">Update</Button>
</StackPanel>
This is my ViewModel:
private Customer customer;
public Customer Customer
{
get { return customer; }
set { customer = value; NotifyOfPropertyChange(() => Customer); }
}
public bool CanUpdateClick
{
get
{
if (string.IsNullOrEmpty(customer.Name))
{
return false;
}
return true;
}
}
public void UpdateClick()
{
//...
}
And this is my model:
private string name;
public string Name
{
get { return name; }
set { name = value; NotifyOfPropertyChange(() => Name); }
}
So I have UpdateClick method and it works perfectly. I also have CanUpdateClick property, but it doesn't work and I don't know why? Button on the UI should be disabled when the textbox is empty. Please help!

You can subscribe to the PropertyChanged event of the Customer class (since you seem to be subclassing PropertyChangedBase) and call NotifyOfPropertyChanged(() => CanUpdateClick) when the Name property is changed:
// in your view model
// i'm assuming here that Customer is set before your view model is activated
protected override void OnActivate()
{
base.OnActivate();
Customer.PropertyChanged += CustomerPropertyChangedHandler;
}
protected override void OnDeactivate(bool close)
{
base.OnDeactivate(close);
// unregister handler
Customer.PropertyChanged -= CustomerPropertyChangedHandler;
}
// event handler
protected void CustomerPropertyChangedHandler(object sender, PropertyChangedEventArgs e)
{
if (e.PropertyName == nameof(Customer.Name))
{
NotifyOfPropertyChange(() => CanUpdateClick);
}
}
Or, you can just create a Name or CustomerName property in your view model to bind to and a) use Customer.Name as your backing field or b) use a normal backing field then just set Customer.Name when updating:
In your view:
<TextBox Text="{Binding CustomerName, UpdateSourceTrigger=PropertyChanged}" Width="136"/>
And here's how you implement option a in your view model:
public string CustomerName
{
get { return Customer.Name; }
set
{
Customer.Name = value;
NotifyOfPropertyChange(); // CallerMemberName goodness
NotifyOfPropertyChange(() => CanUpdateClick);
}
}

Related

Changes in Property that implements INotifyPropertyChanged not being reflected in UI

I am trying to implement the MVVM Pattern but unfortunately is taking longer than expected.
I have a ListView populated by an ObservableCollection of ContactsVm, Adding or Removing Contacts works perfectly, the problem comes when trying to change only one Item from this collection by selecting it.
The Xaml where I am setting my bindings:
<ListView ItemsSource="{Binding ContactsToDisplay}"
SelectedItem="{Binding SelectedContact, Mode=TwoWay}"
SeparatorColor="Black"
ItemSelected="OnItemSelected">
<ListView.ItemTemplate>
<DataTemplate>
<TextCell Text="{Binding FirstName}"
Detail="{Binding Id}">
<TextCell.ContextActions>
<MenuItem
Text="Delete"
IsDestructive="true"
Clicked="Delete_OnClicked"
CommandParameter="{Binding .}" />
</TextCell.ContextActions>
</TextCell>
</DataTemplate>
</ListView.ItemTemplate>
</ListView>
Its cs:
public ContactBookApp()
{
InitializeComponent();
MapperConfiguration config = new MapperConfiguration(cfg => {
cfg.CreateMap<Contact, ContactVm>();
cfg.CreateMap<ContactVm, Contact>();
});
BindingContext = new ContactBookViewModel(new ContactService(), new PageService(), new Mapper(config));
}
private void AddButton_OnClicked(object sender, EventArgs e)
{
(BindingContext as ContactBookViewModel)?.AddContact();
}
private void OnItemSelected(object sender, SelectedItemChangedEventArgs e)
{
(BindingContext as ContactBookViewModel)?.SelectContact(e.SelectedItem as ContactVm);
}
private void Delete_OnClicked(object sender, EventArgs e)
{
(BindingContext as ContactBookViewModel)?.DeleteContact((sender as MenuItem)?.CommandParameter as ContactVm);
}
}
My ViewModel, here the "problematic" part is the SelectContact method, I am posting the rest in case it helps:
public class ContactBookViewModel : BaseViewModel
{
private readonly IContactService _contactService;
private readonly IPageService _pageService;
private readonly IMapper _mapper;
private ContactVm _selectedContact;
public ObservableCollection<ContactVm> ContactsToDisplay { get; set; }
public ContactVm SelectedContact
{
get => _selectedContact;
set => SetValue(ref _selectedContact, value);
}
public ContactBookViewModel(IContactService contactService, IPageService pageService, IMapper mapper)
{
_contactService = contactService;
_pageService = pageService;
_mapper = mapper;
LoadContacts();
}
private void LoadContacts()
{
List<Contact> contactsFromService = _contactService.GetContacts();
List<ContactVm> contactsToDisplay = _mapper.Map<List<Contact>, List<ContactVm>>(contactsFromService);
ContactsToDisplay = new ObservableCollection<ContactVm>(contactsToDisplay);
}
public void SelectContact(ContactVm contact)
{
if (contact == null)
return;
//None of this approaches works:
//SelectedContact.FirstName = "Test";
//contact.FirstName = "Test;
}
}
}
My ContactVm class:
public class ContactVm : BaseViewModel
{
private string _firstName;
public int Id { get; set; }
public string FirstName
{
get => _firstName;
set => SetValue(ref _firstName, value);
}
}
The BaseViewModel:
public class BaseViewModel
{
public event PropertyChangedEventHandler PropertyChanged;
[NotifyPropertyChangedInvocator]
protected virtual void OnPropertyChanged([CallerMemberName] string propertyName = null)
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
protected void SetValue<T>(ref T backingField, T value, [CallerMemberName]string propertyName = null)
{
if (EqualityComparer<T>.Default.Equals(backingField, value))
return;
backingField = value;
OnPropertyChanged(propertyName);
}
}
As you can see, I am trying to update each selected contact setting its FirstName = "Test", the changed are updated but unfortunately they are not getting reflected in the UI, hope you can help me to find what I am doing wrong.
Thanks in advance!
Your BaseViewModel does not implement the INotifyPropertyChanged interface.
Since you had used MVVM , you could handle the logic diretly in your ViewModel when you select item in listview (you don't need to define ItemSelected event any more) .
private ContactVm _selectedContact;
public ContactVm SelectedContact
{
set
{
if (_selectedContact!= value)
{
_selectedContact= value;
SelectedContact.FirstName="Test";
NotifyPropertyChanged("SelectedContact");
}
}
get { return _selectedContact; }
}
And don't forget to implement the INotifyPropertyChanged to your model and viewmodel.
I guess the NotifyPropertyChangedInvocator attribute is not properly notifying the property changes. But I am not sure about that. Because your BaseViewModel does not implement the INotifyPropertyChanged interface.
The below code works fine for me. This is how I use it in my entire project.
I have directly derived the INotifyPropertyChanged interface in my BaseModel and implemented the property changes.
public class BaseModel : INotifyPropertyChanged
{
public event PropertyChangedEventHandler PropertyChanged;
public void NotifyPropertyChanged([System.Runtime.CompilerServices.CallerMemberName] string propertyName = "")
{
if (this.PropertyChanged != null)
{
this.PropertyChanged(this, new PropertyChangedEventArgs(propertyName));
}
}
}
public class ContactVm : BaseModel
{
private string _firstName;
public int Id { get; set; }
public string FirstName
{
get { return _firstName; }
set
{
this._firstName = value;
NotifyPropertyChanged();
}
}
}
This is what I have in my callback.
public void SelectContact(ContactVm contact)
{
if (contact == null)
return;
contact.FirstName = "Test";
}
The only difference is I have implemented property changes for the ObservableCollection in ViewModel too.
public ObservableCollection<ContactVm> ContactsToDisplay
{
get { return _contactsToDisplay; }
set
{
this._contactsToDisplay = value;
NotifyPropertyChanged();
}
}
Note that I have not used your SelectedContact binding in my case. May be as you said that binding would be the issue.
I hope it helps you.

Caliburn Micro, Wpf binding nested model

Hi i have an application Wpf with Caliburn Micro and MongoDb
i have a collection like this
[Bson IgnoreExtraElements]
public class ResourceCollection : CompanyModel
{
public ResourceCollection(string Vat) : base(Vat)
{
}
private long _ResourceID;
public long ResourceID
{
get { return _ResourceID; }
set { _ResourceID = value; }
}
private string _Description;
public string Description
{
get { return _Description; }
set
{
_Description = value;
NotifyOfPropertyChange(() => Description);
}
}
}
where CompanyModel inherit from PropertyChangedBase, and i have a view model:
public class ResourceCreateViewModel : Screen
{
private IWindowManager _windowManager;
private readonly AppConnection _appConnection;
private readonly ResourceRepository _resourceRepository;
private ResourceCollection _Resource;
public ResourceCollection Resource
{
get
{
return _Resource;
}
set
{
_Resource = value;
NotifyOfPropertyChange(() => Resource);
NotifyOfPropertyChange(() => CanSave);
}
}
}
And this is my xaml
<TextBox Text="{Binding Resource.Description, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}" Height="20" DockPanel.Dock="Right"></TextBox>
my problem is that when i change the value inside the texbox, the set of my viewmodel class not fire, how can i bind my class to the textbox?
Thank you in advance
The reason it doesn't fire it is simple: the Resource object didn't get set, you only set a property on it. To solve this you could create a new property ResourceDescription and bind to that instead:
public string ResourceDescription
{
get
{
return _Resource.Description;
}
set
{
_Resource.Description = value;
NotifyOfPropertyChange(() => ResourceDescription);
NotifyOfPropertyChange(() => Resource);
NotifyOfPropertyChange(() => CanSave);
}
}
Xaml:
<TextBox Text="{Binding Resource.Description, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}"
But this comes with its own problems because changes in the viewmodel no longer update your view. Instead you can subscribe to the resources PropertyChanged event:
private ResourceCollection _Resource;
public ResourceCollection Resource
{
get
{
return _Resource;
}
set
{
if(_Resource != null)
{
_Resource.PropertyChanged -= ResourcePropertyChanged;
}
_Resource = value;
if(_Resource != null)
{
_Resource.PropertyChanged += ResourcePropertyChanged;
}
NotifyOfPropertyChange(() => Resource);
NotifyOfPropertyChange(() => CanSave);
}
}
private void ResourcePropertyChanged(object sender, EventArgs e)
{
//you might be able to do something better than just notify of changes here
NotifyOfPropertyChange(() => Resource);
NotifyOfPropertyChange(() => CanSave);
}
This can get complicated very quickly, especially if you are subscribing to properties nested deeper in the object graph.

How to handle changes of submodels in main model (MVVM)

What is the best practice to update dynamically created checkboxes states from model? Acutal values for checkboxes are held in submodels of main model and being changed accordingly to it's logic. Checkboxes' properties bind to their individual FooViewModels. But how to change FooViewModel's properties then?
1 way: main model fires especial event -> main VM handles it and finds target FooViewModel to update using event args -> main VM sets target FooViewModel properties with values specified in event args -> checkbox is updated via bindings to FooViewModel
2 way: Main model holds observable collection of FooModels implementing INPC and each is being wrapped with FooViewModel (using CollectionChanged event in main VM). Main model set some FooModel's property -> FooViewModel handles PropertyChanged and transfers it further firing own PropertyChanged event -> checkbox is updated via bindings to FooViewModel.
Transferrence code in FooViewModel:
this._model.PropertyChanged += (s, a) => this.RaisePropertyChangedEvent(a.PropertyName);
My implementation of 2nd way is next:
// MainModel class that holds collection of extra models (CfgActionModel):
class MainModel: BindableBase
{
ObservableCollection<CfgActionModel> _actionsColl
= new ObservableCollection<CfgActionModel>();
public ObservableCollection<CfgActionModel> ActionCollection
{
get => this._actionsColl;
}
public void AddAction(ConfigEntry cfgEntry, bool isMeta)
{
CfgActionModel actionModel = new CfgActionModel()
{
CfgEntry = cfgEntry,
Content = cfgEntry.ToString(),
IsEnabled = true,
IsChecked = false
};
this._actionsColl.Add(actionModel);
}
}
// Extra model that is wrapped with CfgActionViewModel:
class CfgActionModel: BindableBase
{
ConfigEntry _cfgEntry; // Custom enumeration value unique for each checkbox
string _content;
bool _isEnabled = false;
bool _isChecked = false;
public ConfigEntry CfgEntry
{
get => this._cfgEntry;
set
{
if (this._cfgEntry == value) return;
this._cfgEntry = value;
this.RaisePropertyChangedEvent(nameof(CfgEntry));
}
}
public string Content
{
get => this._content;
set
{
if (this._content == value) return;
this._content = value;
this.RaisePropertyChangedEvent(nameof(Content));
}
}
public bool IsEnabled
{
get => this._isEnabled;
set
{
if (this._isEnabled == value) return;
this._isEnabled = value;
this.RaisePropertyChangedEvent(nameof(IsEnabled));
}
}
public bool IsChecked
{
get => this._isChecked;
set
{
if (this._isChecked == value) return;
this._isChecked = value;
this.RaisePropertyChangedEvent(nameof(IsChecked));
}
}
}
// CfgActionViewModel that is checkbox in UI is bound to:
class CfgActionViewModel: BindableBase
{
CfgActionModel _model;
public CfgActionViewModel(CfgActionModel model)
{
this._model = model;
this._model.PropertyChanged += (s, a) => this.RaisePropertyChangedEvent(a.PropertyName);
}
public string Content
{
get => this._model.Content;
set => this._model.Content = value;
}
public bool IsEnabled
{
get => this._model.IsEnabled;
set => this._model.IsEnabled = value;
}
public bool IsChecked
{
get => this._model.IsChecked;
set => this._model.IsChecked = value;
}
}
// MainViewModel where we fill the model with data:
class MainViewModel
{
MainModel model;
readonly ObservableCollection<CfgActionViewModel> _actionVMColl = new ObservableCollection<CfgActionViewModel>();
public ObservableCollection<CfgActionViewModel> ActionVMCollection => this._actionVMColl;
public MainViewModel()
{
this.model = new MainModel();
this.model.ActionCollection.CollectionChanged += (s, a) =>
{
// when new model is created we create new ViewModel wrapping it
if (a.Action == NotifyCollectionChangedAction.Add)
{
CfgActionModel newModel = (CfgActionModel) a.NewItems[0];
CfgActionViewModel actionViewModel = new CfgActionViewModel(newModel);
_actionVMColl.Add(actionViewModel);
}
};
model.AddAction(ConfigEntry.AutoBuy, false);
model.AddAction(ConfigEntry.Bomb, false);
}
}
DataTemplate in View looks like this:
<DataTemplate DataType="{x:Type mvvm:CfgActionViewModel}">
<CheckBox
IsChecked="{Binding Path=IsChecked, Mode=TwoWay}"
IsEnabled="{Binding Path=IsEnabled, Mode=TwoWay}"
Content="{Binding Path=Content, Mode=OneWay}"/>
</DataTemplate>
Is it acceptable by MVVM to avoid interaction with MainViewModel somewhere (2nd way) or each subViewModel's property must be set by MainViewModel (1st way)?
Both approaches are acceptable. But personally, I would do approach #1 to keep my Models as thin as possible.
You can refer to the sample code on how you can do approach #1.
public class MainViewModel : BindableBase
{
public ObservableCollection<SubViewModel> SubViewModels { get; }
public MainViewModel()
{
SubViewModels = new ObservableCollection<SubViewModel>();
SubViewModels.CollectionChanged += SubViewModels_CollectionChanged;
}
private void SubViewModels_CollectionChanged(object sender, NotifyCollectionChangedEventArgs e)
{
if(e.Action == NotifyCollectionChangedAction.Add)
{
foreach(var subVM in e.NewItems.Cast<SubViewModel>())
{
subVM.PropertyChanged += SubViewModel_PropertyChanged;
}
}
// TODO: Unsubscribe to SubViewModels that are removed in collection to avoid memory leak.
}
private void SubViewModel_PropertyChanged(object sender, PropertyChangedEventArgs e)
{
switch (e.PropertyName)
{
case nameof(SubViewModel.IsChecked):
// TODO: Do your thing here...
break;
}
}
}
public class SubViewModel : BindableBase
{
private bool _isChecked;
public bool IsChecked
{
get => _isChecked;
set => SetProperty(ref _isChecked, value);
}
}
As you can see, I don't even need to include any Models in the sample code which means that all the logic here are all clearly part of the presentation layer.
Now, you can focus on your business/domain logic in your Models.

C# Wpf Combobox set selectedvalue, not change selecteditem in UI

i have a combobox inside a usercontrol
<ComboBox DisplayMemberPath="CustomerCollection.Description1" SelectedValuePath="CustomerCollection.CustomerID"
SelectedValue="{Binding Order.OrderCollection.CustomerID, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}"
ItemsSource="{Binding Customers, UpdateSourceTrigger=PropertyChanged}">
</ComboBox>
My view model have a class model Order, that have a property CustomerID, and inside the view model, i have an observableCollection of customers
private OrderModel _Order;
public OrderModel Order
{
get
{
return _Order;
}
set
{
SetProperty(ref _Order, value);
}
}
private ObservableCollection<CustomerModel> _Customers;
public ObservableCollection<CustomerModel> Customers
{
get { return _Customers; }
set { SetProperty(ref _Customers, value); }
}
Everything works, but when i set the Order.OrderCollection.CustomerID, so the SelectedValue of my combobox, in the ui not update the selecteditem
private void ReloadCustomers(object CustomerID)
{
DialogVisibility = System.Windows.Visibility.Visible;
BackgroundWorker bgwLoad = new BackgroundWorker();
bgwLoad.DoWork += delegate (object s, DoWorkEventArgs args)
{
Customers = new ObservableCollection<CustomerModel>(_customerRepository.Get());
};
bgwLoad.RunWorkerCompleted += (sender, eventArgs) =>
{
Order.OrderCollection.CustomerID = (long)CustomerID;
_eventAggregator.GetEvent<UIX_MessageEventAggregator.PassParameter>().Unsubscribe(ReloadCustomers);
};
bgwLoad.RunWorkerAsync();
}
i try to implement the override of equals inside Customer, but the stil not working, the only way to get work, seems to create a property of customer inside the viewmodel and binding to selecteditem, but i don't like
public override bool Equals(object obj)
{
var customer = obj as CustomerModel;
if (customer != null)
return customer.CustomerCollection.CustomerID == CustomerCollection.CustomerID;
else
return false;
}
public override int GetHashCode()
{
return CustomerCollection.CustomerID.GetHashCode();
}
i know i missing something, but i can't understand what?
Someone have idea?
public class OrderCollection : _CompanyModel
{
public OrderCollection(string Vat) : base(Vat)
{
}
#region Core
private string _OrderID;
public string OrderID
{
get { return _OrderID; }
set { SetProperty(ref _OrderID, value); }
}
private long _CustomerID;
public long CustomerID
{
get { return _CustomerID; }
set { SetProperty(ref _CustomerID, value); }
}
#endregion
}
Ok the problem is the BackgroundWorker
This work:
private void ReloadCustomers(object CustomerID)
{
DialogVisibility = System.Windows.Visibility.Visible;
Customers = new ObservableCollection<CustomerModel>(_customerRepository.Get());
Order.OrderCollection.CustomerID = (long)CustomerID;
}
This not:
private void ReloadCustomers(object CustomerID)
{
DialogVisibility = System.Windows.Visibility.Visible;
BackgroundWorker bgwLoad = new BackgroundWorker();
bgwLoad.DoWork += delegate (object s, DoWorkEventArgs args)
{
Customers = new ObservableCollection<CustomerModel>(_customerRepository.Get());
};
bgwLoad.RunWorkerCompleted += (sender, eventArgs) =>
{
Order.OrderCollection.CustomerID = (long)CustomerID;
_eventAggregator.GetEvent<UIX_MessageEventAggregator.PassParameter>().Unsubscribe(ReloadCustomers);
};
bgwLoad.RunWorkerAsync();
}
as you did not post the content of the OrderCollection class this is just an assumption. But my best guess is that you don't raise a PropertyChangedEvent when you set the CustomerID property in the OrderCollection.
Therefore the View never gets notified about the change in selection.
So changing the model implementation to raise a property changed even for your CustomID should fix the issue.
However you might not want to pollute your Model with such events. Another option could be to wrap your models (Customer, Order) in dedicated ViewModels and raise the events in there where needed.
The issue was inside BackgroundWorker, i've repleced with
await Task.Run(() =>
{
Customers = new ObservableCollection<CustomerModel>(_customerRepository.Get());
})
.ContinueWith(t =>
{
Order.OrderCollection.CustomerID = (long)CustomerID;
_eventAggregator.GetEvent<UIX_GlobalEvent.PassParameter>().Unsubscribe(ReloadCustomers);
});

Model changes updating View-Model WPF

I am having an issue getting my model changes updated back into my viewmodel so i can display. In this example i have a label and a button, when i press the button it will execute some business logic, and should update the label on screen. However, when my model changes the view will not. Any Idea on what i am doing wrong here?
View-
<Window.DataContext>
<vm:ViewModel>
</Window.DataContext>
<Grid>
<Label Content="{Binding Path=Name}"/>
<Button Command={Binding UpdateBtnPressed}/>
</Grid>
ViewModel
public ViewModel()
{
_Model = new Model();
}
public string Name
{
get{return _Model.Name;}
set
{
_Model.Name = value;
OnPropertyChanged("Name");
}
}
public ICommand UpdateBtnPressed
{
get{
_UpdateBtn = new RelayCommand(param => UpdateLabelValue());
return _UpdateBtn;
}
private void UpdateLabelValue()
{
_Model.Name = "Value Updated";
}
Model
private string name = "unmodified string";
public string Name
{
get{return name;}
set{name = value;}
}
Try this:
private void UpdateLabelValue()
{
Name = "Value Updated";
}
It seems you've missed to implement the INotifyPropertyChanged interface.
Your model must implement INotifyPropertyChanged such as;
public class Personel : INotifyPropertyChanged
{
public event PropertyChangedEventHandler PropertyChanged;
private string _name;
public string Name
{
get { return _name; }
set { _name = value; OnChanged("Name");}
}
void OnChanged(string pn)
{
if (PropertyChanged != null)
{
PropertyChanged(this, new PropertyChangedEventArgs(pn));
}
}
}

Categories