I have a class Device that uses the INotifyPropertyChanged, it is tested and it works.
Now I have a deviceMonitor that is the UI representation of this device. In the code I have a reference to Device and I want to link changes in the device to changes in the UI (two way is not needed, but clicking the deviceMonitor should call a certain function of the device)
I'm using expression Blend with VS2015 so guidance based on where to click to get it to work would be extremely welcome.
this is a mockup of the device
public class Device : INotifyPropertyChanged
{
public string Name { ... } //uses NotifyPropertyChanged in the set
// other properties and their relative private vars.
}
Then the xaml.cs for the GUI, here I have a reference to the dll containing the Device:
public partial class DeviceControl : UserControl
{
public Device myDevice = new Device();
public DeviceControl()
{
InitializeComponent();
// here I tried setting the datacontest to the myDevice
// also tried to set the dataContext in Blend and here grab a
// reference to it and store it in myDevice. But nothing workerd
}
public void ChangeDevName()
{
this.myDevice.DeviceName = "Test";
//UI Representation of deviceName never changed
}
}
Then the XAML
<UserControl>
<UserControl.DataContext>
<recoveriX:RecoverixDevice DeviceName="thisIsAName"/>
</UserControl.DataContext>
<Grid>
<TextBlock x:Name="title" Text="{Binding DeviceName}"/>
</Grid>
</UserControl>
This might work:
In your DeviceControl UserControl, wire up events for OnLoaded and OnUnloaded of the control.
In the code-behind for the event handlers, subscribe/unsubscribe to the PropertyChanged event of UserControl's DataContext (this.DataContext) ; like so:
private void OnLoaded(object sender, RoutedEventArgs e)
{
if (this.DataContext is INotifyPropertyChanged)
{
((INotifyPropertyChanged)this.DataContext).PropertyChanged += OnDataContextPropertyChanged;
}
}
private void OnUnloaded(object sender, RoutedEventArgs e)
{
if (this.DataContext is INotifyPropertyChanged)
{
((INotifyPropertyChanged)this.DataContext).PropertyChanged -= OnDataContextPropertyChanged;
}
}
private void OnDataContextPropertyChanged(object sender, PropertyChangedEventArgs e)
{
// You could also just update every time something is changed.
// As an example you could check for the "Name" property being changed.
if (e.PropertyName == nameof(Device.Name))
{
title.Text = this.DataContext.Name;
}
}
An important note to make is the if (this.myDevice is INotifyPropertyChanged) check.
It ensures the Device class inherits from INotifyPropertyChanged.
Providing it does, it casts the Device being your DataContext (this.DataContext) to (INotifyPropertyChanged) so you can subscribe to the PropertyChanged event from the INotifyPropertyChanged interface.
Then, when a property on the DataContext is changed, your handler will be fired. Obviously you can put what you want to do in the code of OnMyDevicePropertyChanged, I've just used "Name" as an example.
Hope this helps!
EDIT
Furthermore; you could also store a private field of type Device in the UserControl's code-behind. A bit like so:
private Device _viewModel; // You could also use the interface (like 'IDevice'), too.
Then in your `OnLoaded' event, store it in the field:
if (this.DataContext is INotifyPropertyChanged)
{
this.viewModel = this.DataContext;
// Wire up your PropertyChanged handler as before.
}
And on your OnUnloaded event, just unsubscribe from the viewModel if it is not null:
if (this.viewModel != null)
{
this.viewModel.PropertyChanged -= OnDataContextPropertyChanged;
}
This also gives you a bit more flexibility when you've got the DataContext stored as a field, as you can use it within other methods (if you use any more in your code behind - you shouldn't...; but it saves CPU time casting it to INotifyPropertyChanged all the time.
For future reference I would look at Implementing MVVM Practices into your projects.
Good luck!
Problem was overwriting the private device, setting the datacontext fixed the thing.
This is the final class:
public partial class DeviceControl : UserControl
private Device _device = new Device();
public DeviceControl()
{
InitializeComponnents();
this.DataContext = _device;
}
public void SetDevice(Device d)
{
//This fails:
//_device = d;
//This works
this.DataContext = d;
}
Related
I have a WPF project which has a main window containing a tab control which, in turn, contains several user controls.
I need the main window to display a warning when the value of certain properties have changed on the various user controls. I am using an event handler to populate a TextBlock when these properties change.
The remaining problem I have is that when the application is started, and the different UI properties get assigned an initial value, this assignment seems to be triggering the event. I only want to display this warning when the user has changed something.
Here's what I have:
An abstract class which facilitates the event
public class ViewModelBase {
Boolean isBusy;
protected virtual void OnConfigurationChanged(EventArgs args) {
EventHandler handler = ConfigurationChanged;
handler?.Invoke(this, args);
}
public event EventHandler ConfigurationChanged;
}
User controls which trigger the event when applicable properties change
class MyViewModel : ViewModelBase {
String myTextField;
public MyTextField {
get => myTextField;
set {
myTextField = value;
OnConfigurationChanged(null);
}
}
}
In my main window, I instantiate the user control and subscribe to the event. I also have the unsaved changes property in here
class MainWindowVM : ViewModelBase {
String unSavedChanges;
public MainWindowVM() {
MyViewModel MyVM = new MyViewModel();
MyVM.ConfigurationChanged += onConfigurationChanged;
}
String UnsavedChanges {
get => unSavedChanges;
set {
unSavedChanges = value;
OnPropertyChanged(nameof(UnsavedChanges));
}
}
void onConfigurationChanged(Object sender, EventArgs args) {
UnsavedChanges = "Configuration not saved.";
}
}
A XAML TextBlock bound to UnsavedChanges
<TextBlock Text="{Binding UnsavedChanges}"
Foreground="Red"
Margin="10"/>
I guess this is obvious to most folks, but for anyone else that struggles with this issue:
In order for an initial value to be reflected in a property-bound UI component, the value must be assigned to the underlying field in the object constructor. Changes to the field outside of the constructor will not be seen by the corresponding property and, in turn, will have no effect on the property-bound UI component.
Assigning a value to a property which invokes OnPropertyChanged() on set will always trigger the subscribed event. The constructor is not exempt in this regard.
I'm using x:Bind and INotifyPropertyChanged to update UI in UWP application. But it behaves like OneTime binding even though I set it to OneWay.
Bindings.Update() works, but I want to know why INotifyPropertyChanged fails.
XAML
<TextBlock Text="{x:Bind staffVM.Name, Mode=OneWay}"/>
Code-behind:
private StaffViewModel staffVM;
private void ListView_SelectionChanged(object sender, SelectionChangedEventArgs e)
{
// I want to change staffVM according to ListView's selection.
staffVM = staffListView.SelectedItem as StaffViewModel;
staffVM.Update(); // If change this to Bindings.Update(), It works.
}
ViewModel:
public class StaffViewModel: INotifyPropertyChanged
{
private Character character;
public string Name => character.name == string.Empty ? null : character.name;
public void Update()
{
RaisePropertyChanged(string.Empty);
}
public event PropertyChangedEventHandler PropertyChanged;
public void RaisePropertyChanged([CallerMemberName]string propName = null)
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propName));
}
}
Firstly, you need to specify the name of the variable that you want to update:
public void Update()
{
RaisePropertyChanged(nameof(Name));
}
Documentation and sample: https://learn.microsoft.com/en-us/uwp/api/windows.ui.xaml.data.inotifypropertychanged.propertychanged
Secondly, by default x:Bind is OneTime
To fix it, add Mode="OneWay"
Mode Specifies the binding mode, as one of these strings: "OneTime", "OneWay", or "TwoWay". The default is "OneTime". Note that this differs from the default for {Binding}, which is "OneWay" in most cases.
Please read documentation
https://learn.microsoft.com/en-us/windows/uwp/xaml-platform/x-bind-markup-extension
The problem here is not on the level of the StaffViewModel class, but on the level of the page. When you do:
staffVM = staffListView.SelectedItem as StaffViewModel;
The UI has no notification about the fact that the staffVM field has changed. So the binding is still pointing to the old instance of StaffViewModel. Hence when you do staffVM.Update(), it does notify about changes, but the UI is not listening to that instance - it is still listening to notifications on the first selected item. Bindings.Update() fixes this because it completely re-evaluates all bindings so it will "get" the new value of staffVM field.
Solution would be to implement INotifyPropertyChanged on the Page and encapsulate the staffVM in a property which raises PropertyChanged event.
Ideally I would however suggest creating a "root" view model, which you will set only once and will not change and which will contain the selected item as its property. This way you don't have to implement INotifyPropertyChanged in the Page and its code-behind will be simpler. As a result you will have something like the following in the code-behind:
public RootViewModel VM {get;} = new RootViewModel();
And in XAML:
<TextBlock Text="{x:Bind VM.SelectedStaff.Name, Mode=OneWay}"/>
My label only seems to get the data from the property it is bound to once. I have the Property raising the Property Changed event in the setter, but when the value of the property gets changed, it raises the event properly (I know this because of the break point I set), but the text in the Label on the window doesn't change. I should maybe also note that the window with the label isn't the main window, but a new one that pops up.
ViewModel:
public class PurchaseVerificationViewModel : ViewModelBase
{
private WindowService.WindowService windowService = new WindowService.WindowService();
private string _verificationQuestion = "Question"; //default so i can check if it changed in the window
public string VerificationQuestion
{
get { return _verificationQuestion; }
set
{
if (_verificationQuestion != value)
{
_verificationQuestion = value;
OnPropertyChanged(nameof(VerificationQuestion));
}
}
}
}
Window:
<Window>
<Window.DataContext>
<viewmodels:PurchaseVerificationViewModel/>
</Window.DataContext>
<Grid>
<Label Content="{Binding VerificationQuestion, UpdateSourceTrigger=PropertyChanged}"/>
</Grid>
</Window>
ViewModelBase:
public class ViewModelBase : INotifyPropertyChanged
{
public event PropertyChangedEventHandler PropertyChanged;
public void OnPropertyChanged(string propertyName)
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
}
There's no problem with your implementation of the INotifyPropertyChanged, since you are correctly invoking the notification that your property has been modified.
So if the problem is not with the one who's raising the notification, might it rather be with what is actively listening to it?
And the problem is that you're defining the DataContext of your Window to the class itself, rather than to the instance which you are utilizing and modifying in the code-behind of your application.
What is actually happening under the hoods, due to the way you defined your DataContext in xaml, is that a new PurchaseVerificationViewModel class is being constructed (is the not the one who are modifying on your logic) and therefore your VerificationQuestion will return it's default value (or rather the private backing field default value, "Question").
In reality the problem is that you have induced your listener to listen to the wrong thing.
Since you want the content of the Label (target) to be update based on a source change, what you have to do, is to set as the DataContextof the Window the specific instance which you are modifying on the logic of your application, and make sure you define it as a property!
public PurchaseVerificationViewModel myViewModel {get;set;}
For instance after InitializeComponent(), on your page constructor, you could initialize the property and set it as the DataContext, like this:
myViewModel = new PurchaseVerificationViewModel();
this.DataContext = myViewModel;
I'm have implemented a custom TextBox:
public class MyTextBox : TextBox
{
// ...
}
that I'm using from XAML:
<MyTextBox Text="{Binding MyProperty}" />
and it's bound to a property in my ViewModel.
public class MyDataContext : INotifyPropertyChanged
{
public string MyProperty
{
get { return _myPropertyBackingField; }
set
{
_myPropertyBackingField = value;
PropertyChanged(this, new PropertyChangedEventArgs("MyProperty"));
}
}
// ...
}
Question: How can I, in MyTextBox, detect that MyProperty is changed?
MyProperty = "NewValue";
Preferably, I would like to distinguish a programmatical change from when the change was triggered by the user editing the value. That is, I don't think overriding OnPropertyChanged works for me.
You can register to the PropertyChanged event of the TextBox's DataContext.
var dataContext = DataContext as MyDataContext;
dataContext.PropertyChanged += dataContext_PropertyChanged;
// check for the propertyname and react
void dataContext_PropertyChanged(object sender, PropertyChangedEventArgs e)
{
if (e.PropertyName == "MyProperty")
{
// Do things
}
}
So if your viewmodel raises PropertyChanged you textbox also gets notified. But I think that's bad practice. What do you want to achieve?
OP here.
I realised after a while that it's the binding that keeps track of updating the TextBox from the source (DataContext). So a possible path to take would be to call GetBindingExpression(TextProperty) and work something out from that.
However, I solved it by overriding TextBoxBase.OnTextChanged:
protected override void OnTextChanged(TextChangedEventArgs e)
{
base.OnTextChanged(e);
if (!IsFocused)
{
// Do stuff here
}
}
Since the control is not focused, the change must have been done programatically. This is not perfect since a programatical change might come when the TextBox has focus, but it is good enough for me.
A further question to Clemen's fine answer here: DataContext values in view code behind. If one used this approach, is it possible to detect property changes on the VM at this point? These are correctly implemented through INotifyPropertyChanged.
var viewModel = DataContext as MyViewModel;
//How would one detect a property change on viewModel?
//Tried viewModel.PropertyChange which doesn't fire.
I think you must be doing something wrong that you're not mentioning in your post. The following code works as expected and will print MyTestPropertyName to the Console window.
public partial class MainWindow : Window
{
public MainWindow()
{
DataContext = new MyViewModel();
}
private void Window_Loaded(object sender, RoutedEventArgs e)
{
MyViewModel viewModel = DataContext as MyViewModel;
viewModel.PropertyChanged += MyPropertyChangedEventHandler;
viewModel.NotifyPropertyChanged();
}
private void MyPropertyChangedEventHandler(object sender, PropertyChangedEventArgs e)
{
Console.WriteLine(e.PropertyName);
}
}
public class MyViewModel : INotifyPropertyChanged
{
public void NotifyPropertyChanged()
{
PropertyChangedEventHandler handler = PropertyChanged;
if (handler != null)
{
handler(this, new PropertyChangedEventArgs("MyTestPropertyName"));
}
}
#region INotifyPropertyChanged Members
public event PropertyChangedEventHandler PropertyChanged;
#endregion
}
It should be noted that this is TERRIBLE design, and is only designed as a proof of concept, that you can indeed subscribe to events on the ViewModel in the code-behind.
You would need to either subscribe to the PropertyChanged event of each dependency property (I.e. the properties that implement INotifyPropertyChanged), or modify your MyViewModel class to raise an event from the setters of the properties (dependency or otherwise) that you are interested in being notified about, and then subscribe to the common event.