Caliburn Micro, Wpf binding nested model - c#

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.

Related

AvaloniaUI StyledProperty and MVVM binding doesn't update

I'm using AvaloniaUI and making a UserControl with a StyledProperty using. I'm using MVVM.
The problem is that when I have a Binding to my styledproperty but it doesn't update.
I want to use the UserControl like this <Comp:ValueTextBlock VariableName="{Binding RobotSettingsModel.Robot_SP}"/> where VariableName uses a binding to a model that is created in the ViewModel.
The problem is that I can't seem to get the StyledProperty to work when I use a binding. When I set VariableName directly in the view it does work
// this works
<Comp:ValueTextBlock VariableName="PLC_U1_Robot_SP"/>
// this doesn't
<Comp:ValueTextBlock VariableName="{Binding RobotSettingsModel.Robot_SP}" DescriptionLocation="Left"/>
What am I doing wrong here?
The code-behind for my UserControl ValueTextBlock looks something like this:
public class ValueTextBlock : UserControl
{
private ValueTextBlockVm _viewModel;
#region --- Variable name properties ---
public static readonly StyledProperty<string> VariableNameProperty = AvaloniaProperty.Register<ValueTextBlock, string>(nameof(VariableName), defaultBindingMode: BindingMode.TwoWay, defaultValue: "UNKNOWN DProperty");
public string VariableName
{
get { return _viewModel.vmVariableName; } // the property is used in the ViewModel
set { _viewModel.vmVariableName = value; }
}
#endregion
#region constructor
public ValueTextBlock()
{
this.InitializeComponent();
// create new instance of viewmodel and attach it as DataContext
_viewModel = new ValueTextBlockVm();
this.DataContext = _viewModel;
}
private void InitializeComponent()
{
AvaloniaXamlLoader.Load(this);
}
#endregion
}
In the ViewModel for ValueTextBlock vmVariableName is done like this:
private string _vmVariableName = "UKN";
public string vmVariableName
{
get => _vmVariableName;
set => this.RaiseAndSetIfChanged(ref _vmVariableName, value);
}
When i use this UserControl and directly set VariableName in view it works, but when I use Binding it doesn't work.
This is my view:
<StackPanel>
<Comp:ValueTextBlock VariableName="PLC_U1_Robot_SP"/> <!-- directly setting VariableName works -->
<Comp:ValueTextBlock VariableName="{Binding RobotSettingsModel.Robot_SP}" DescriptionLocation="Left"/> <!-- binding VariableName to a model doesn't work -->
<TextBlock Text="{Binding RobotSettingsModel.Robot_SP}"/> <!-- binding normal text to a model does work-->
</StackPanel>
Code-behind for the view
public class ucRobotSettings : UserControl
{
private ucRobotSettingsVm _viewModel;
#region properties
public string Prefix
{
get { return _viewModel.vmPrefix; }
set { _viewModel.vmPrefix = value; }
}
public static readonly StyledProperty<string> PrefixProperty = AvaloniaProperty.Register<ucRobotSettings, string>(nameof(Prefix));
#endregion
#region constructor
public ucRobotSettings()
{
this.InitializeComponent();
// create new instance of viewmodel and attach it as DataContext
_viewModel = new ucRobotSettingsVm();
this.DataContext = _viewModel;
this.AttachedToVisualTree += ucRobotSettings_AttachedToVisualTree;
}
private void InitializeComponent()
{
AvaloniaXamlLoader.Load(this);
}
#endregion
private void ucRobotSettings_AttachedToVisualTree(object sender, VisualTreeAttachmentEventArgs e)
{
_viewModel.OnAttachedToVisualTree();
}
}
In the ViewModel a new RobotSettingsModel is made, this is what I want to bind to in the View
public class ucRobotSettingsVm : ViewModelBase
{
#region --- properties ---
private string _vmPrefix;
public string vmPrefix
{
get => _vmPrefix;
set => this.RaiseAndSetIfChanged(ref _vmPrefix, value);
}
private RobotSettingsModel _RobotSettingsModel = new RobotSettingsModel();
public RobotSettingsModel RobotSettingsModel
{
get => _RobotSettingsModel;
set => this.RaiseAndSetIfChanged(ref _RobotSettingsModel, value);
}
#endregion
public ucRobotSettingsVm() { }
public void OnAttachedToVisualTree()
{
// don't update if the prefix hasn't changed
if (RobotSettingsModel.Prefix != vmPrefix) RobotSettingsModel.Prefix = vmPrefix;
}
}
The model that is used in ucRobotSettings looks like this:
public class RobotSettingsModel : ReactiveObject
{
// unit prefix.
private string _Prefix;
public string Prefix { get => _Prefix; set { this.RaiseAndSetIfChanged(ref _Prefix, value); NotifyPropertyChanged(); } }
// values
private string _Robot_SP = "UKN";
public string Robot_SP { get => _Robot_SP; set => this.RaiseAndSetIfChanged(ref _Robot_SP, value); }
public RobotSettingsModel()
{ }
public event PropertyChangedEventHandler PropertyChanged;
private void NotifyPropertyChanged([System.Runtime.CompilerServices.CallerMemberName] String propertyName = "")
{
if (propertyName == "Prefix") // only update when property "Prefix changes"
{
Robot_SP = Prefix + "_" + nameof(Robot_SP);
// inform outside outside world the complete class has PropertyChanged
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
}
}

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.

Async method call on ComboBox selection with MVVM

I'm facing a problem with displaying graphs filtered by ComboBox selection without having the UI lock up. The statistic filtering is quite heavy and needs to run async. This works fine all up until I try to call FilterStatisticsAsync and MonthSelectionChanged from the Property setter. Does anyone have a good tip on how to solve or work around this?
The XAML looks like this:
<ComboBox x:Name="cmbMonth"
ItemsSource="{Binding Months}"
SelectedItem="{Binding SelectedMonth }"
IsEditable="True"
IsReadOnly="True"
And the ViewModel property setter like this:
public string SelectedMonth
{
get { return _selectedMonth; }
set { SetProperty(ref _selectedMonth, value); LoadStatisticsAsync(); MonthSelectionChanged(); }
}
SetProperty derives from a base class which encapsulates INPC like this:
public event PropertyChangedEventHandler PropertyChanged = delegate { };
protected virtual void SetProperty<T>(ref T member, T value, [CallerMemberName] string propertyName = null)
{
if (Equals(member, value))
return;
member = value;
PropertyChanged(this, new PropertyChangedEventArgs(propertyName));
}
I would do it using this:
public class AsyncProperty<T> : INotifyPropertyChanged
{
public async Task UpdateAsync(Task<T> updateAction)
{
LastException = null;
IsUpdating = true;
try
{
Value = await updateAction.ConfigureAwait(false);
}
catch (Exception e)
{
LastException = e;
Value = default(T);
}
IsUpdating = false;
}
private T _value;
public T Value
{
get { return _value; }
set
{
if (Equals(value, _value)) return;
_value = value;
OnPropertyChanged();
}
}
private bool _isUpdating;
public bool IsUpdating
{
get { return _isUpdating; }
set
{
if (value == _isUpdating) return;
_isUpdating = value;
OnPropertyChanged();
}
}
private Exception _lastException;
public Exception LastException
{
get { return _lastException; }
set
{
if (Equals(value, _lastException)) return;
_lastException = value;
OnPropertyChanged();
}
}
public event PropertyChangedEventHandler PropertyChanged;
[NotifyPropertyChangedInvocator]
protected virtual void OnPropertyChanged([CallerMemberName] string propertyName = null)
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
}
Definition of property
public AsyncProperty<string> SelectedMonth { get; } = new AsyncProperty<string>();
somewhere else in your code:
SelectedMonth.UpdateAsync(Task.Run(() => whateveryourbackground work is));
binding in xaml:
SelectedItem="{Binding SelectedMonth.Value }"
Note that properties should reflect a current state, instead of triggering processes which may take an indefinite amount of time. Hence the need to update the property in a different way from just assigning it.

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

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

Dual binding on Checkbox

I have a WPF application using MVVM. I have the IsChecked value bound to a boolean on my model instance on my ViewModel. I also need to bind a method on the ViewModel to the Checked and Unchecked events. (This is so I can track unsaved changes and change the background to give my users visual indication of the need to save. I tried:
<CheckBox
Content="Enable"
Margin="5"
IsChecked="{Binding Enabled}"
Checked="{Binding ScheduleChanged}"
Unchecked="{Binding ScheduleChanged}"
/>
But I get a 'Provide value on 'System.Windows.Data.Binding' threw an exception.' error. Advice?
Here is the Model I am working with:
public class Schedule : IEquatable<Schedule>
{
private DateTime _scheduledStart;
private DateTime _scheduledEnd;
private bool _enabled;
private string _url;
public DateTime ScheduledStart
{
get { return _scheduledStart; }
set
{
_scheduledStart = value;
}
}
public DateTime ScheduledEnd
{
get { return _scheduledEnd; }
set
{
if(value < ScheduledStart)
{
throw new ArgumentException("Scheduled End cannot be earlier than Scheduled Start.");
}
else
{
_scheduledEnd = value;
}
}
}
public bool Enabled
{
get { return _enabled; }
set { _enabled = value; }
}
public string Url
{
get { return _url; }
set { _url = value; }
}
public bool Equals(Schedule other)
{
if(this.ScheduledStart == other.ScheduledStart && this.ScheduledEnd == other.ScheduledEnd
&& this.Enabled == other.Enabled && this.Url == other.Url)
{
return true;
}
else
{
return false;
}
}
}
My viewModel contains a property that has an ObservableCollection. An ItemsControl binds to the collection and generates a list. So my ViewModel sort of knows about my Model instance, but wouldn't know which one, I don't think.
Checked and Unchecked are events, so you can not bind to them like you can IsChecked, which is a property. On a higher level it is also probably wise for your view model not to know about a checkbox on the view.
I would create an event on the view model that fires when Enabled is changed, and you can subscribe to that and handle it any way you like.
private bool _enabled;
public bool Enabled
{
get
{
return _enabled;
}
set
{
if (_enabled != value)
{
_enabled = value;
RaisePropertyChanged("Enabled");
if (EnabledChanged != null)
{
EnabledChanged(this, EventArgs.Empty);
}
}
}
}
public event EventHandler EnabledChanged;
// constructor
public ViewModel()
{
this.EnabledChanged += This_EnabledChanged;
}
private This_EnabledChanged(object sender, EventArgs e)
{
// do stuff here
}
You should be able to just handle this in the setter for Enabled...
public class MyViewModel : ViewModelBase
{
private bool _isDirty;
private bool _enabled;
public MyViewModel()
{
SaveCommand = new RelayCommand(Save, CanSave);
}
public ICommand SaveCommand { get; }
private void Save()
{
//TODO: Add your saving logic
}
private bool CanSave()
{
return IsDirty;
}
public bool IsDirty
{
get { return _isDirty; }
private set
{
if (_isDirty != value)
{
RaisePropertyChanged();
}
}
}
public bool Enabled
{
get { return _enabled; }
set
{
if (_enabled != value)
{
_enabled = value;
IsDirty = true;
}
//Whatever code you need to raise the INotifyPropertyChanged.PropertyChanged event
RaisePropertyChanged();
}
}
}
You're getting a binding error because you can't bind a control event directly to a method call.
Edit: Added a more complete example.
The example uses the MVVM Lite framework, but the approach should work with any MVVM implementation.

Categories