AvaloniaUI StyledProperty and MVVM binding doesn't update - c#

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

Related

Binding to an object property in ViewModel, or how to use multiple models with the same property names

I am trying to bind to a property of a Model via the ViewModel in an textbox. I created a wrapper in the ViewModel to access the property of the Model. Whenever I put a break point in the get section of this wrapper it triggers, but a break point in the set section is not executed after changing the textbox content. Code is according to the following tutorial: https://www.codeproject.com/Articles/1193164/MVVM-Sample-application-with-a-focus-in-property
The BaseViewModel implements the INPC interface. I am trying to follow the MVVM "rule of thumbs" as close as possible. So no INPC implementation in the model in this case, and the view can only bind to the VM.
What am I doing wrong?
XAML:
<TextBox Grid.Column="1" VerticalAlignment="Center" Text="{Binding Path=ProgramInfo.Name, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}" />
Model:
public class ProgramInfo
{
private string _name;
private string _description;
public string Name
{
get => _name;
set
{
_name = value;
}
}
public string Description
{
get => _description;
set
{
_description = value;
}
}
}
ViewModel:
class ProgramInfoViewModel : BaseViewModel
{
private ProgramInfo _programInfo;
public ProgramInfo ProgramInfo
{
get => _programInfo;
set
{
_programInfo = value;
OnPropertyChanged("ProgramInfo");
}
}
}
Code-behind:
public partial class MainWindow : Window
{
public MainWindow()
{
InitializeComponent();
DataContext = new ProgramInfoViewModel();
}
}
_programInfo is always null. you need to create an instance:
class ProgramInfoViewModel : BaseViewModel
{
private ProgramInfo _programInfo = new ProgramInfo();
public ProgramInfo ProgramInfo
{
get => _programInfo;
set
{
_programInfo = value;
OnPropertyChanged("ProgramInfo");
}
}
}
or
public partial class MainWindow : Window
{
public MainWindow()
{
InitializeComponent();
DataContext = new ProgramInfoViewModel { ProgramInfo = new ProgramInfo() };
}
}
a view model wrapper for model property will look like this:
class ProgramInfoViewModel : BaseViewModel
{
private ProgramInfo _programInfo;
private ProgramInfoViewModel(ProgramInfo programInfo)
{
_programInfo = programInfo;
}
public string ProgramInfoName
{
get => _programInfo.Name;
set
{
_programInfo.Name = value;
OnPropertyChanged("ProgramInfoName");
}
}
}
initialization:
public partial class MainWindow : Window
{
public MainWindow()
{
InitializeComponent();
var model = new ProgramInfo();
DataContext = new ProgramInfoViewModel(model);
}
}
binding path has to change accordingly:
<TextBox Text="{Binding Path=ProgramInfoName, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}" />

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.

MVVM DataBinding OK in Designer, not at runtime

Why can databinding be seen working in the designer:
Click to show image: Databinding seems OK
But runtime shows nothing?
Click to show image: No Data, no usercontrol?
Outline code structure:
ViewModelBase : baseclass inheriting from INotofyPropertychanged
public class ViewModelBase : INotifyPropertyChanged
{
public event PropertyChangedEventHandler PropertyChanged;
protected virtual void OnPropertyChanged([CallerMemberName] string propertyName = null)
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
protected virtual bool SetProperty<T>(ref T storage, T value, [CallerMemberName] string propertyName = "")
{
if (EqualityComparer<T>.Default.Equals(storage, value))
return false;
storage = value;
this.OnPropertyChanged(propertyName);
return true;
}
}
SiteViewModel : Model class with Id/Name/Description Properties
public class SiteViewModel : ViewModelBase
{
private int _SiteID;
private string _Name;
private string _Description;
public int SiteID
{
get { return _SiteID; }
set { SetProperty(ref _SiteID, value); }
}
public string Name
{
get { return _Name; }
set { SetProperty(ref _Name, value); }
}
public string Description
{
get { return _Description; }
set { SetProperty(ref _Description, value); }
}
}
SitesViewModel: ObservableCollection of SiteViewModel
public class SitesViewModel : ViewModelBase
{
private ObservableCollection<SiteViewModel> _AllSites;
public ObservableCollection<SiteViewModel> AllSites {
get { return _AllSites; }
set { SetProperty<ObservableCollection<SiteViewModel>>(ref _AllSites, value); }
}
public SitesViewModel()
{
AllSites = new ObservableCollection<SiteViewModel>();
for (int count = 1; count <= 3; count++)
{
AllSites.Add(new SiteViewModel { SiteID = count, Name = "Test" + count.ToString(), Description = "Site:" + count.ToString() } );
}
}
}
SiteManagerControl : UserControl with a SitesViewModel property _AllSites
public partial class SiteManagerControl : UserControl
{
private SitesViewModel _AllSites;
public SitesViewModel AllSites
{
get { return _AllSites; } //<-- Breakpoint not hit!
set {
if (_AllSites != value)
{ _AllSites = value;
OnPropertyChanged("AllSites");
}}
}
public SiteManagerControl(){
_AllSites = new SitesViewModel();}
(XAML can be seen in the first linked image above, Note the breakpoint not hit line in the above). The user control is hosted in a Tabcontrol that is part of an ObservableCollection. I don't think this is an issue in the databinding. Will post the code for the tabs if needed.
There are no errors in the Debug Output window to indicate why the databinding is failing.
Your listview DataContext Data binding is with object from class(SitesViewModel)
This class has property named (AllSites) that has oveservable collection property named (AllSites) as well.
so I think you have to fix the ItemSource binding in the list view like this:
ItemsSource="{Binding AllSites.AllSites}"
Will's comment (thanks!) above pointed me in the right direction: Changed MainWindow.xaml to contain:
<DataTemplate DataType="{x:Type vm:SitesViewModel}">
<uc:SitesView></uc:SitesView>
</DataTemplate>
Also followed this : http://codingtales.blogspot.co.uk/2010/02/creating-complete-tabbed-interface-in.html to rework my tab interface

subscribing to property changes in View throws ArgumentNullException

I have code that looks similar to the following
ViewModel:
public class VM : ReactiveObject
{
public double _number;
public double Number
{
get { return _number; }
set { this.RaiseAndSetIfChanged(ref _number, value); }
}
}
View:
public sealed partial class MainPage : Page, IViewFor<VM>
{
public MainPage()
{
ViewModel = new VM();
DataContext = ViewModel;
this.InitializeComponent();
this.WhenAnyValue(t => t.ViewModel.Number)
.Subscribe(n => Debug.WriteLine(n));
}
public VM ViewModel
{
get { return (VM)GetValue(ViewModelProperty); }
set { SetValue(ViewModelProperty, value); }
}
public static readonly DependencyProperty ViewModelProperty =
DependencyProperty.Register("ViewModel", typeof(VM), typeof(MainPage), new PropertyMetadata(null));
object IViewFor.ViewModel
{
get { return ViewModel; }
set { ViewModel = (VM)value; }
}
private void Button_Click(object sender, RoutedEventArgs e)
{
ViewModel.Number = ViewModel.Number + 1;
}
}
When I run it I get an ArgumentNullException on the "WhenAnyValue" line.
This seems to follow every example of usage I can find. I'm at a loss here.
Make ViewModel a DependencyProperty and your problems will go away

How to find out that a refrence type property changed in viewmodel?

I have a UserControl like this:
<Button Grid.Row="1" X:Name="ChangeButton" Click="ChangeButton_Click">Change</Button>
My UserControl CodeBehind:
public static readonly DependencyProperty SelectedPersonProperty =
DependencyProperty.Register("SelectedPerson", typeof(PersonModel), typeof(MyControl),
new FrameworkPropertyMetadata
{
DefaultValue=null,
BindsTwoWayByDefault = true
});
public PersonModel SelectedPerson
{
get { return (PersonModel)GetValue(SelectedPersonProperty); }
set { SetValue(SelectedPersonProperty, value);}
}
public static readonly DependencyProperty TextValueProperty =
DependencyProperty.Register("TextValue", typeof(string), typeof(NumericTextBox),
new FrameworkPropertyMetadata{
BindsTwoWayByDefault=true
});
public string TextValue
{
get { return (string)GetValue(TextValueProperty); }
set {
SetValue(TextValueProperty, value);
}
}
public MyControl(){
SelectedPerson = new PersonModel();
}
private void ChangeButton_Click(object sender, RoutedEventArgs e)
{
TextValue="Hello";
SelectedPerson.Age = 12;
SelectedPerson.FirstName = "AA";
SelectedPerson.LastName = "BB";
}
In my MainView Xaml Code i have :
<UC:MyControl SelectedPerson="{Binding Person}" TextValue="{Binding Path=Name,UpdateSourceTrigger=PropertyChanged}" ></UC:MyControl>
<TextBox Grid.Row="1" Background="Yellow" Text="{Binding Path=Person.Name,UpdateSourceTrigger=PropertyChanged}" />
MainViewModel Code:
PersonModel person
public PersonModel Person {
get { return _person; }
set
{
if (_person!= value)//i set a breakpoint here
{
_person= value;
RaisePropertyChanged("Person ");
}
}
}
string _name;
public string Name
{
get { return _name; }
set
{
if (_name != value)//i set a breakpoint here.when TextValue changed this breakepoint actived
{
_name = value;
RaisePropertyChanged("Name");
}
}
}
when i click on ChangeButton in my user control the selectedperson in usercontrol changed and my Textbox in my view show Name like "AA".but my breake point in viewmode is not actived.how can i find out that when my Person in viewmode changed?
You need to implement INotifyPropertyChanged inside the PersonModel and and set a break point on the property that you change instead of the entire Person object property. Like so
class PersonModel : INotifyPropertyChanged
{
public event PropertyChangedEventHandler PropertyChanged = delegate { };
string name;
public string Name
{
get { return name; }
set
{
name = value; <---- set breakpoint here
PropertyChanged(this, new PropertyChangedEventArgs("Name"));
}
}
}
You can then listen to changes in personmodel class like so:
myParentObject.Person.PropertyChanged += (sender,e)=>
{
//do something here
}
To hit breakpoint for the Person setter you need to update ChangeButton_Click as following:
private void ChangeButton_Click(object sender, RoutedEventArgs e)
{
TextValue="Hello";
PersonModel newPerson = new PersonModel();
newPerson.Age = 12;
newPerson.FirstName = "AA";
newPerson.LastName = "BB";
SelectedPerson = newPerson;
}
What you were doing was just modifying properties on existing PersonModel object. This way you will not hit breakpoint at PersonModel object setter but you will hit breakpoint on properties setter inside PersonModel class. By modifying code as above you will hit you breakpoint.

Categories