I am at my wits' end. Newbie to WPF so unsure where I am going wrong.
I have an MS Word Interop Add-in that captures selected text, along with various metadata. It composes my model and passes the data to it.
For this stage of development, I made my model a singleton just so that I know everything is pointing to the same reference.
Anyhow, my model is composed of a list of entries.
public sealed class MYMODELSingleton : ObservableObject, IMYMODEL
{
private static MYMODELSingleton instance;
private MYMODELSingleton()
{
isActive = true;
EntryList = new List<MyEntry.IMyEntry>();
}
public static MYMODELSingleton Instance
{
get
{
if(instance == null)
{
instance = new MYMODELSingleton();
}
return instance;
}
}
public bool isActive { get; set; }
private List<MyEntry.IMyEntry> _entryList;
public List<MyEntry.IMyEntry> EntryList
{
get { return _entryList; }
set { _entryList = value; OnPropertyChanged("EntryList"); }
}
public void Add(IMYEntry mEntry)
{
try {
EntryList.Add(mEntry);
OnPropertyChanged("EntryList");
}
catch(ArgumentException ae)
{
throw ae;
}
}
ObservableObject is a custom class I made that implements INotifyPropertyChanged
public abstract class ObservableObject : INotifyPropertyChanged
{
#region INotifyPropertyChanged Members
/// <summary>
/// Raised when a property on this object has a new value.
/// </summary>
public event PropertyChangedEventHandler PropertyChanged;
/// <summary>
/// Raises this object's PropertyChanged event.
/// </summary>
/// <param name="propertyName">The property that has a new value.</param>
protected virtual void OnPropertyChanged(string propertyName)
{
#if DEBUG
MessageBox.Show("Inside Event!");
#endif
var handler = this.PropertyChanged;
if (handler != null)
{
#if DEBUG
MessageBox.Show("Event Fired!");
#endif
var e = new PropertyChangedEventArgs(propertyName);
handler(this, e);
}
}
#endregion // INotifyPropertyChanged Members
}
This is my ViewModel
public class MyEntryViewModel : ObservableObject
{
private MYMODELSingleton activeMYMODEL= MYMODELSingleton.Instance;
public MyProjectModel.MYMODEL.MYMODELSingleton ActiveMYMODEL
{
get
{
return activeMYMODEL;
}
set
{
activeMYMODEL = value;
OnPropertyChanged("ActiveMYMODEL");
}
}
private void ModelPropertyChanged(object sender, PropertyChangedEventArgs e)
{
if (e.PropertyName == "EntryList") {
int index = activeMYMODEL.EntryList.Count - 1;
MyEntry = activeMYMODEL.EntryList[index];
UpdateGui(MyEntry.Source);
}
}
public void UpdateGui(SelectionState selState)
{
TxtDocName = selState.SelectionDocName;
TxtDocPage = selState.SelectionPage;
TxtDocText = selState.SelectionText;
}
public MyEntryViewModel()
{
this.MyEntry = new MyEntry();
activeMYMODEL.PropertyChanged += new PropertyChangedEventHandler(ModelPropertyChanged);
//This is to notify me when the VM is created
#if DEBUG
System.Windows.Forms.MessageBox.Show("ViewModelOpened!");
#endif
}
~MyEntryViewModel()
{
activeMYMODEL.PropertyChanged -= new PropertyChangedEventHandler(this.ModelPropertyChanged);
#if DEBUG
System.Windows.Forms.MessageBox.Show("ViewModel Closed!");
#endif
}
}
}
Relevant WPF Code
xmlns:custns="clr-namespace:MyProjectViewModel.MyEntryViewModel;assembly=MyProjectViewModel"
Title="MyProject" Height="350" Width="525" SizeToContent="Height">
<Window.Resources>
<custns:MyyEntryViewModel x:Key="MyProjectObj" />
</Window.Resources>
<Grid Name="gridEntry" DataContext="{StaticResource MyProjectObj}" >
<TextBlock x:Name="docNameTxtBx" Grid.Row="0" Grid.Column="1" HorizontalAlignment="Stretch" VerticalAlignment="Center" Margin="20" TextWrapping="Wrap" Text="{Binding TxtDocName, UpdateSourceTrigger=PropertyChanged}" />
</Grid>
The problem is, while the entry gets added to mymodel's composed List no problem, the handler in OnPropertyChanged() always equals null. This, despite the fact that I know the ViewModel is instantiated (due to the messageboxes popping up).
FWIW, Model, ViewModel, WPF, and AddIn are all in different assemblies. The Viewmodel subscribes to the Model's event in its constructor, so why is my handler null when it's called???
UPDATE
After discussing this with a colleague, I realized the issue lies in how the MS Word ADDIN and the VM both compose the Model. Both are running in different threads and, apparently, both are holding separate instances of my Singleton! I am not sure how that is happening. I even changed my Singleton's code, making the private instance "volatile" and adding a static Object that I then lock when Instance.get() is called.
private static volatile MyModelSingleton instance;
private static object syncRoot = new Object();
private MyModelSingleton()
{
isActive = true;
EntryList = new List<MyEntry.IMyFEntry>();
}
public static MyModelSingleton Instance
{
get
{
lock (syncRoot)
{
if (instance == null)
{
instance = new MyModelSingleton();
}
}
return instance;
}
}
Nonetheless, both the Word Interop Addin and the Viewmodel are holding separate instances. What am I missing?
in the wpf code you presented, you never bind against ActiveMYMODEL. becuase it is never bound in you xaml, there is never a hook into you notify property changed event created.
Related
I am attempting to make an undo system, where when a property on an object that implements INotifyPropertyChanged is changed, the property name and its old value is pushed onto a stack via a KeyValuePair. When the user clicks "Undo" it then pops from the stack and uses reflection to set the property's value to its old value.
The problem with this is that it calls OnPropertyChanged again, so the property and its restored value is added to the undo stack a second time. On the other hand, I still want it to call OnPropertyChanged since I want the view to update its bindings.
There's obviously something wrong with how I'm designing it, but I can't seem to figure out another way of going about it.
Here's my model
internal class MyModel : INotifyPropertyChangedExtended
{
private string testProperty1 = "";
public string TestProperty1
{
get { return testProperty1; }
set {
var oldValue = testProperty1;
testProperty1 = value;
OnPropertyChanged(nameof(TestProperty1), oldValue);
}
}
private string testProperty2 = "";
public string TestProperty2
{
get { return testProperty2; }
set {
var oldValue = testProperty2;
testProperty2 = value;
OnPropertyChanged(nameof(TestProperty2), oldValue);
}
}
public event PropertyChangedEventHandler? PropertyChanged;
public void OnPropertyChanged(string propertyName, object oldValue)
{
if (PropertyChanged != null)
{
PropertyChanged(this, new PropertyChangedEventArgsExtended(propertyName, oldValue));
}
}
}
Here's my INotifyPropertyChangedExtended interface
public class PropertyChangedEventArgsExtended : PropertyChangedEventArgs
{
public virtual object OldValue { get; private set; }
public PropertyChangedEventArgsExtended(string propertyName, object oldValue)
: base(propertyName)
{
OldValue = oldValue;
}
}
public class INotifyPropertyChangedExtended : INotifyPropertyChanged
{
public event PropertyChangedEventHandler PropertyChanged;
protected void NotifyPropertyChanged(string propertyName, object oldValue)
{
if (PropertyChanged != null)
PropertyChanged(this, new PropertyChangedEventArgsExtended(propertyName, oldValue));
}
}
And here's my view model
internal class MyViewModel
{
public MyModel MyModel { get; set; } = new();
public Stack<KeyValuePair<string, object>> PropertyStateStack = new();
public RelayCommand Undo { get; set; }
public MyViewModel()
{
SetupCommands();
MyModel.PropertyChanged += MyModel_PropertyChanged;
}
private void MyModel_PropertyChanged(object? sender, System.ComponentModel.PropertyChangedEventArgs e)
{
var args = e as PropertyChangedEventArgsExtended;
if (args.OldValue != null)
{
PropertyStateStack.Push(new KeyValuePair<string, object>(args.PropertyName, args.OldValue));
}
}
private void SetupCommands()
{
Undo = new RelayCommand(o =>
{
KeyValuePair<string, object> propertyState = PropertyStateStack.Pop();
PropertyInfo? property = MyModel.GetType().GetProperty(propertyState.Key);
if (property != null)
{
property.SetValue(MyModel, Convert.ChangeType(propertyState.Value, property.PropertyType), null);
}
});
}
}
EDIT: I did research the "memento pattern" but I couldn't get it to work with INotifyPropertyChanged, since as soon as I set MyModel to a backup of it the bindings to the view stopped working.
Implementing Memento or a variant is the right way. Opposed to storing the particular modifying undo action e.g., Action<T> (another good solution), Memento has a higher memory footprint (as it stores the complete object state), but allows random access to the stored states.
The key point is that when implementing Memento properly, you don't have to rely on reflection, which will only make your code slow and heavy.
The following example uses the IEditableObject interface to implement the Memento pattern (variant). The implementation supports undo and redo. The TextBox class is implementing undo/redo in a similar way using the same interface. The advantage is that you have full control over when to record the object's state. You can even cancel the ongoing modification.
This example clones the complete object to backup the state. Because objects can be quite expensive, for example when they allocate resources, it could make sense to introduce an immutable data model that actually stores the values of the public editable properties. Now, instead of cloning the complete object you would only clone the immutable data model. This can improve the performance in critical scenarios.
See the example provided by the IEditableObject link above to learn how to introduce an immutable data model that holds the object's data.
The actual undo/redo logic is encapsulated in the example's abstract StateTracker<TStateObject> class. StateTracker<TStateObject> implements the aforementioned IEditableObject and the ICloneable interface. To add convenience, StateTracker<TStateObject> also implements a custom IUndoable interface (to enable anonymous usage of the public undo/redo API).
Every class that needs to support state tracking (undo/redo) must extend the abstract StateTracker<TStateObject> to provide a ICloneable.Clone and a StateTracker.UpdateState implementation.
The following example is very basic. It allows undo and redo, but does not support random access to undo/redo states. You would have to use an index based backing store like List<T> to implement such a feature.
IUndoable.cs
Enable anonymous access to the undo/redo API.
public interface IUndoable
{
bool TryUndo();
bool TryRedo();
}
StateTracker.cs
Encapsulates the actual undo/redo logic to avoid duplicate implementations
for each type that is supposed to support undo/redo.
You can consider to add a public UndoCommand and RedoCommand to this class and let the commands invoke TryUndo and TryRedo respectively.
public abstract class StateTracker<TStateObject> : IEditableObject, IUndoable, ICloneable
{
public bool IsInEditMode { get; private set; }
private Stack<TStateObject> UndoMemory { get; }
private Stack<TStateObject> RedoMemory { get; }
private TStateObject StateBeforeEdit { get; set; }
private bool IsUpdatingState { get; set; }
protected StateTracker()
{
this.UndoMemory = new Stack<TStateObject>();
this.RedoMemory = new Stack<TStateObject>();
}
public abstract TStateObject Clone();
protected abstract void UpdateState(TStateObject state);
object ICloneable.Clone() => Clone();
public bool TryUndo()
{
if (!this.UndoMemory.TryPop(out TStateObject previousState))
{
return false;
}
this.IsUpdatingState = true;
this.StateBeforeEdit = Clone();
this.RedoMemory.Push(this.StateBeforeEdit);
UpdateState(previousState);
this.IsUpdatingState = false;
return true;
}
public bool TryRedo()
{
if (!this.RedoMemory.TryPop(out TStateObject nextState))
{
return false;
}
this.IsUpdatingState = true;
this.StateBeforeEdit = Clone();
this.UndoMemory.Push(this.StateBeforeEdit);
UpdateState(nextState);
this.IsUpdatingState = false;
return true;
}
// Start recording the changes
public void BeginEdit()
{
if (this.IsInEditMode || this.IsUpdatingState)
{
return;
}
this.IsInEditMode = true;
// Create the snapshot before the instance is changed
this.StateBeforeEdit = Clone();
}
// Abort recording the changes
public void CancelEdit()
{
if (!this.IsInEditMode)
{
return;
}
// Restore the original state
UpdateState(this.StateBeforeEdit);
this.IsInEditMode = false;
}
// Commit recorded changes
public void EndEdit()
{
if (!this.IsInEditMode || this.IsUpdatingState)
{
return;
}
// Commit the snapshot of the original state after the instance was changed without cancellation
this.UndoMemory.Push(this.StateBeforeEdit);
this.IsInEditMode = false;
}
}
MyModel.cs
public class MyModel : StateTracker<MyModel>, INotifyPropertyChanged
{
public event PropertyChangedEventHandler PropertyChanged;
public MyModel()
{
}
// Copy constructor
private MyModel(MyModel originalInstance)
{
// Don't raise PropertyChanged to avoid the loop of death
this.testProperty1 = originalInstance.TestProperty1;
this.testProperty2 = originalInstance.TestProperty2;
}
// Create a deep copy using the copy constructor
public override MyModel Clone()
{
var copyOfInstance = new MyModel(this);
return copyOfInstance;
}
protected override void UpdateState(MyModel state)
{
// UpdateState() is called by the StateTracker
// which internally guards against the infinite loop
this.TestProperty1 = state.TestProperty1;
this.TestProperty2 = state.TestProperty2;
}
private void OnPropertyChanged([CallerMemberName] string propertyName = null)
=> this.PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
private string testProperty1;
public string TestProperty1
{
get => this.testProperty1;
set
{
this.testProperty1 = value;
OnPropertyChanged();
}
}
private string testProperty2;
public string TestProperty2
{
get => this.testProperty2;
set
{
this.testProperty2 = value;
OnPropertyChanged();
}
}
}
Example
The following example stores the state of a TextBox, that binds to a MyModel instance. When the TextBox receives focus, the MyModel.BeginEdit method is called to start recording the input. When the TextBox loses focus the recorded state is pushed onto the undo stack by calling the MyModel.EndEdit method.
MainWindow.xaml
<Window>
<Window.DataContext>
<local:MyModel />
</Window.DataContext>
<StackPanel>
<Button Content="Undo"
Click="OnUndoButtonClick" />
<Button Content="Redo"
Click="OnRedoButtonClick" />
<TextBox Text="{Binding TestProperty1, UpdateSourceTrigger=PropertyChanged}"
GotFocus="OnTextBoxGotFocus"
LostFocus="OnTextBoxLostFocus" />
</StackPanel>
</Window>
MainWindow.xaml.cs
Because of the defined interfaces we can handle undo/redo without knowing the actual data type.
private void OnTextBoxGotFocus(object sender, RoutedEventArgs e)
=> ((sender as FrameworkElement).DataContext as IEditableObject).BeginEdit();
private void OnTextBoxLostFocus(object sender, RoutedEventArgs e)
=> ((sender as FrameworkElement).DataContext as IEditableObject).EndEdit();
private void OnUndoButtonClick(object sender, RoutedEventArgs e)
=> _ = ((sender as FrameworkElement).DataContext as IUndoable).TryUndo();
private void OnRedoButtonClick(object sender, RoutedEventArgs e)
=> _ = ((sender as FrameworkElement).DataContext as IUndoable).TryRedo();
An alternative flow could be that the MyModel class internally calls BeginEdit and EndEdit inside the relevant property setters (before accepting the new value and after accepting the new value). In case of the TextBox, the advantage of this solution is that it allows to record every single input.
In this scenario, the GotFocus and LostFocus event handlers previously defined on the TextBox (example above) are not needed and related code must be removed:
MyModel.cs
public class MyModel : StateTracker<MyModel>, INotifyPropertyChanged
{
public event PropertyChangedEventHandler PropertyChanged;
public MyModel()
{
}
// Copy constructor
private MyModel(MyModel originalInstance)
{
// Don't raise PropertyChanged to avoid the loop of death
this.testProperty1 = originalInstance.TestProperty1;
this.testProperty2 = originalInstance.TestProperty2;
}
// Create a deep copy using the copy constructor
public override MyModel Clone()
{
var copyOfInstance = new MyModel(this);
return copyOfInstance;
}
protected override void UpdateState(MyModel state)
{
// UpdateState() is called by the StateTracker
// which internally guards against the infinite loop
this.TestProperty1 = state.TestProperty1;
this.TestProperty2 = state.TestProperty2;
}
private void RecordPropertyChange<TValue>(ref TValue backingField, TValue newValue)
{
BeginEdit();
backingField = newValue;
EndEdit();
}
private void OnPropertyChanged([CallerMemberName] string propertyName = null)
=> this.PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
private string testProperty1;
public string TestProperty1
{
get => this.testProperty1;
set
{
RecordPropertyChange(ref this.testProperty1, value);
OnPropertyChanged();
}
}
private string testProperty2;
public string TestProperty2
{
get => this.testProperty2;
set
{
RecordPropertyChange(ref this.testProperty2, value);
OnPropertyChanged();
}
}
}
Remarks
If extending StateTracker is not an option (e.g., because it would introduce a multi-inheritance issue), you can always make use of composition (for example add a private property of type StateTracker to your undoable model to replace inheritance).
Just create a new class that extends StateTracker to implement the abstract members. Then define a private property of this new type in your undoable model. Now, let the model reference this private property to access the undo/redo API.
While composition is to be favored, this example chooses inheritance as this concept feels more natural to most. It may helps to understand the basic idea.
Alright, so I have luck of running into a lot of basic problems. I can't figure a way around this particular issue.
This piece of code needs to access "_Player.Name" property of object created in "MainWindow" class.
Edit: Putting up the whole code this time. Here's the Code_Behind where the string is.
public class Code_Behind
{
private static string _Name = "Default";
public class Player
{
public void setName(string name) //Ignore this part, was trying to find a work around here
{
_Name = name;
}
public string Name
{
get { return _Name; }
set
{
_Name = value;
}
}
}
//contentControl is used to store Content properties
//UI elements are bound to Content properties to efficiently change their Content
public class contentControl : INotifyPropertyChanged
{
protected void OnPropertyChanged(string name)
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(name));
}
public void setEvent(string Event)
{
textBoxContent = Event;
}
public void addEvent(string Event)
{
textBoxContent +="\n" + Event;
}
public class Events
{
public string EV001 = String.Format("\"Greetings {0}. What can I do for you today?\"", window.PlayerName);
}
}
And here is the MainWindow one:
public partial class MainWindow : Window
{
Code_Behind.contentControl cC = new Code_Behind.contentControl();
Code_Behind.contentControl.Events Events = new Code_Behind.contentControl.Events();
Code_Behind.Player _Player = new Code_Behind.Player();
public string GetPlayerName()
{
return _Player.Name;
}
public static string _name = "null";
public MainWindow()
{
this.DataContext = cC;
InitializeComponent();
}
public string GetPlayerName()
{
return _Player.Name
}
Create a method in your MainWindow class. After that you call this method.
public string EV001 = String.Format("\"Greetings {0}. What can I do for you today?\"",
window.GetPlayerName());
You can do it with property too if you want.
public string PlayerName
{
get { return _Player.Name; };
}
The bigger problem you have here is not about accessibility, but not understanding the difference between a class and an object.
MainWindow is a class. It does not represent any specific window. Think of a class like a recipe to create objects. If you had a chocolate chip cookie recipe, you don't eat the recipe, you eat a specific cookie or cookies baked following that recipe.
Your other class first needs to know which specific window you are trying to get the player name from. It needs a reference to a particular MainWindow object.
It looks like you're trying write something like a viewmodel: You've got a player, he has a name, and there's a collection of strings that you think of as "events". I don't understand what the "events" are meant to be, but I implemented my best guess at what I think you seem to be trying to do.
As for this:
public class Events
{
public string EV001 = String.Format("\"Greetings {0}. What can I do for you today?\"", window.PlayerName);
}
I guess you created an instance of MainWindow somewhere, and called it window, but it's defined someplace where it's "out of scope" for that line of code. By analogy, you can't see anything that's behind the next hill, only stuff that's in the valley you're standing in. That's roughly (very roughly, sorry) kind of what scope is about.
But let's move on to my guess at what you're trying to do. This builds, runs, and works. Any questions at all, fire away.
ViewModels.cs
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.ComponentModel;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace Player
{
public class ViewModelBase : INotifyPropertyChanged
{
public event PropertyChangedEventHandler PropertyChanged;
protected void OnPropertyChanged(string name)
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(name));
}
}
public class MainViewModel : ViewModelBase
{
#region Player Property
private PlayerViewModel _player = default(PlayerViewModel);
public PlayerViewModel Player
{
get { return _player; }
set
{
if (value != _player)
{
_player = value;
OnPropertyChanged(nameof(Player));
// Change the player for all the existing events.
foreach (var e in Events)
{
e.Player = Player;
}
}
}
}
#endregion Player Property
private ObservableCollection<Event> _events = new ObservableCollection<Event>();
public ObservableCollection<Event> Events
{
get { return _events; }
private set
{
if (value != _events)
{
_events = value;
OnPropertyChanged(nameof(Events));
}
}
}
#region Event Methods
// This is a BIG guess as to what you're trying to do.
public void AddGreeting()
{
// Player is "in scope" because Player is a property of this class.
if (Player == null)
{
throw new Exception("Player is null. You can't greet a player who's not there.");
}
Events.Add(new Event("\"Greetings {0}. What can I do for you today?\"", Player));
}
#endregion Event Methods
}
public class Employee : ViewModelBase
{
#region DisplayLtdOccupationId Property
private bool _displayLtdOccupationId = default(bool);
public bool DisplayLtdOccupationId
{
get { return _displayLtdOccupationId; }
set
{
if (value != _displayLtdOccupationId)
{
_displayLtdOccupationId = value;
OnPropertyChanged(nameof(DisplayLtdOccupationId));
}
}
}
#endregion DisplayLtdOccupationId Property
}
public class Event : ViewModelBase
{
public Event(String format, PlayerViewModel player)
{
_format = format;
Player = player;
}
private String _format = "";
public String Message
{
get { return String.Format(_format, Player.Name); }
}
#region Player Property
private PlayerViewModel _player = default(PlayerViewModel);
public PlayerViewModel Player
{
get { return _player; }
set
{
if (value != _player)
{
_player = value;
OnPropertyChanged(nameof(Player));
// When player changes, his name changes, so that
// means the value of Message will change.
OnPropertyChanged(nameof(Message));
if (_player != null)
{
_player.PropertyChanged += _player_PropertyChanged;
}
}
}
}
private void _player_PropertyChanged(object sender, PropertyChangedEventArgs e)
{
switch (e.PropertyName)
{
case nameof(PlayerViewModel.Name):
OnPropertyChanged(nameof(Message));
break;
}
}
#endregion Player Property
}
public class PlayerViewModel : ViewModelBase
{
private String _name = default(String);
public String Name
{
get { return _name; }
set
{
if (value != _name)
{
_name = value;
OnPropertyChanged(nameof(Name));
}
}
}
}
}
MainWindow.xaml.cs
using System.Windows;
namespace Player
{
/// <summary>
/// Interaction logic for MainWindow.xaml
/// </summary>
public partial class MainWindow : Window
{
public MainWindow()
{
InitializeComponent();
ViewModel = new MainViewModel();
ViewModel.Player = new PlayerViewModel() { Name = "Ivan the Terrible" };
}
// Just here as a convenience, and to make sure we don't give the DataContext
// the wrong kind of viewmodel.
public MainViewModel ViewModel
{
set { DataContext = value; }
get { return DataContext as MainViewModel; }
}
private void Greeting_Click(object sender, RoutedEventArgs e)
{
ViewModel.AddGreeting();
}
}
}
MainWindow.xaml
<Window
x:Class="Player.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:Player"
mc:Ignorable="d"
Title="MainWindow" Height="350" Width="525">
<Grid>
<StackPanel Orientation="Vertical">
<WrapPanel>
<Button x:Name="Greeting" Content="Greeting" Click="Greeting_Click" />
<Label>Name: </Label>
<TextBox Text="{Binding Player.Name}" Width="120" />
</WrapPanel>
<ListBox
ItemsSource="{Binding Events}"
DisplayMemberPath="Message"
>
</ListBox>
</StackPanel>
</Grid>
</Window>
You can change the set of Name to be private, but still allow the outside world to read the property with the get.
public string Name { get; private set; } = "Default";
This should give you the functionallity desired without the need to create a new GetName() method.
I am having a binding issue I wasn't able to figure out for the past two days. I have thoroughly went through most of the relevant threads on SO, and I still wasn't able to pinpoint where my error lies.
The issue I'm having is with one of the textboxes in my program. The purpose of it is to show the file the user has selected from the file browser. I have bound the text property of it to a string called parameterFileSelected but the textbox never updates even though debugging seems to be showing that the iNotifyPropertyChanged is called and executed properly.
Please help me take a look at my code below if there are any mistakes in my code.
The textbox is part of an xaml called GenerateReports and this view is tied to the GenerateReportsViewModel as follows:
Code for setting datacontext to GenerateReportsViewModel
<Grid >
<Grid.DataContext>
<vm:GenerateReportsViewModel/>
</Grid.DataContext>
<Grid.ColumnDefinitions>
....
Code for TextBox. I have tried removing the Twoway mode, changing it to Oneway and removing the mode but there is no difference.
<TextBox Grid.Column="2" Grid.Row="1" Margin="5" Text="{Binding parameterFileSelected, Mode=Twoway, UpdateSourceTrigger=PropertyChanged}" ></TextBox>
To get the file browser and then to pass the selected file result to the GenerateReportsViewModel, this is the function in the codebehind file. The genviewmodel is initialized in the beginning of the codebehind file as GenerateReportsViewModel genViewModel = new GenerateReportsViewModel();
private void ParaFileButtonClick(object sender, RoutedEventArgs e)
{
OpenFileDialog openFileDialog = new OpenFileDialog();
if (openFileDialog.ShowDialog() == true)
{
DataContext = genViewModel;
genViewModel.updateParameterFileSelected(openFileDialog.FileName.ToString());
}
}
This is the code that's called in GenerateReportsViewModel to update the parameterFileSelected string the textbox is bound to.
class GenerateReportsViewModel : ViewModelBase
{
private string _parameterFileSelected;
public string parameterFileSelected
{
get { return _parameterFileSelected; }
set { SetValue(ref _parameterFileSelected, value); }
}
public void updateParameterFileSelected(string parameterFile)
{
parameterFileSelected = parameterFile;
}
}
Here is the ViewModelBase the viewmodel is attached to.
public class ViewModelBase : INotifyPropertyChanged
{
public event PropertyChangedEventHandler PropertyChanged;
public void SetValue<T>(ref T property, T value, [CallerMemberName] string propertyName = null)
{
if (property != null)
{
if (property.Equals(value)) return;
}
OnPropertyChanged(propertyName);
property = value;
}
protected virtual void OnPropertyChanged([CallerMemberName] string propertyName = null)
{
var handler = PropertyChanged;
if (handler != null) handler(this, new PropertyChangedEventArgs(propertyName));
}
}
}
EDIT
Working Solution after Applying Kevin's Suggestions
For simplicity sake, the Datacontext was set in the XAML.
<Grid>
<Grid.DataContext>
<vm:GenerateReportsViewModel x:Name="generateReportsViewModel"/>
</Grid.DataContext>
Then, I call the string the textbox was bound to, in the viewmodel directly from code behind.
private void ParaFileButtonClick(object sender, RoutedEventArgs e)
{
OpenFileDialog openFileDialog = new OpenFileDialog();
if (openFileDialog.ShowDialog() == true)
{
generateReportsViewModel.parameterFileSelected = openFileDialog.FileName.ToString();
}
}
The ViewModel now uses Kevin's ViewModelBase:
public class GenerateReportsViewModel : ViewModelBase
{
public string parameterFileSelected
{
get { return this.GetValue<string>(); }
set { this.SetValue(value); }
}
}
Thank you Kevin for your solution. Now my 2-day-long problem is solved.
I found out that my previous ViewModelBase was calling iNotifyPropertyChanged but somehow when the View was updated, the value was null instead.
I'm trying to understand why using the ref keyword in your viewModel. I learned a nice way to create the BaseViewModel from the Classon and Baxter book which you can find below. The view-model implements the INotifyPropertyChanged like you did. What you did with [CallerMemberName] is great, it's really magical the way we can reference to our properties thanks to it.
The view model uses a the dictionary to store its properties. It uses a pretty neat trick of looking through the dictionnary keys to see if we contain the string name of the property.Otherwise, we will return a default T value.
public class CommonBaseViewModel: INotifyPropertyChanged
{
private Dictionary<string, object> Values { get; set; }
protected CommonBaseViewModel()
{
this.Values = new Dictionary<string, object>();
}
public event PropertyChangedEventHandler PropertyChanged;
protected T GetValue<T>([CallerMemberName] string name=null)
{
if (this.Values.ContainsKey(name))
{
return (T)this.Values[name];
}
else
{
return default(T);
}
}
protected void SetValue(object value, [CallerMemberName] string name = null)
{
this.Values[name] = value;
//notify my property
this.OnPropertyChanged(new PropertyChangedEventArgs(name));
}
protected void OnPropertyChanged([CallerMemberName] string name=null)
{
this.OnPropertyChanged(new PropertyChangedEventArgs(name));
}
protected virtual void OnPropertyChanged(PropertyChangedEventArgs e)
{
if(this.PropertyChanged != null)
{
this.PropertyChanged(this, e);
}
}
}
As for your GenerateReportViewModel, with the common view model that I provided you, your class then becomes :
public class GenerateReportsViewModel : CommonViewModelBase
{
private string _parameterFileSelected;
public string parameterFileSelected
{
get { return _parameterFileSelected; }
set { SetValue(ref _parameterFileSelected, value); }
}
get
{
return this.GetValue<string>();
}
set
{
this.SetValue(value);
}
public void updateParameterFileSelected(string parameterFile)
{
parameterFileSelected = parameterFile;
}
}
Oh before I forgot, I don't know if it was your intention, but your GenerateReportViewModel is private. This has some impact on your code. Don't forget that by defaut, classes are private!
As for your code behind, even though it could be consider bad practice, I recommend that you have a private field (OpenFileDialog _openFileDialog)that you construct while initializing your page. Because doing it each time your clicking your button is going to consume more data that you need your application to.
//EDIT
I have review my code,and it seemed that the property was not programmed correctly.
public class GenerateReportsViewModel : CommonViewModelBase
{
private string _parameterFileSelected;
public string parameterFileSelected
{
get
{
return this.GetValue<string>();
}
set
{
this.SetValue(value);
}
public void updateParameterFileSelected(string parameterFile)
{
parameterFileSelected = parameterFile;
}
}
More about my comment about constructing the page and binding the view model. While creating your page, you have to create the view-model for that page and then bind it to the data context.
I don't know what you do in your code, but I could provide with this sample such as
public GenerateReportView()
{
InitializeComponent();
//Some operations
var generateReportViewModel = new GenerateReportViewModel();
this.DataContext = generateReportViewModel;
}
I have 2 main classes. The first class represents a Cell that can have values X, O or Empty. I have implemented INotifyPropertyChanged on this.
public class Cell : INotifyPropertyChanged
{
private Symbol state;
public Symbol State
{
get { return state; }
set
{
if (value == Symbol.X || value == Symbol.O)
state = value;
OnPropertyChanged("State");
}
}
public Cell()
{
state = Symbol.Empty;
}
public enum Symbol
{
X, O, Empty
}
public event PropertyChangedEventHandler PropertyChanged;
private void OnPropertyChanged(string propName)
{
if (PropertyChanged != null)
{
PropertyChanged(this, new PropertyChangedEventArgs(propName));
}
}
}
The second class contains an object of this class and is also set as the datacontext for my main window.
public class Board
{
private Cell testCell;
public Cell TestCell
{
get { return testCell; }
set { testCell = value; }
}
public Board()
{
TestCell = new Cell();
}
public void Cell_Click(int cellNum)
{
TestCell.State = Cell.Symbol.O;
}
}
In my mainwindow I have set the datacontext as board, and also contains a button_click function.
public partial class MainWindow : Window
{
public MainWindow()
{
InitializeComponent();
this.DataContext = new Board();
}
private void Button_Click(object sender, RoutedEventArgs e)
{
Board board = this.DataContext as Board;
board.Cell_Click(cellNum);
}
}
In my XAML I have bound to Cell object within board using a button style like this:
<Setter Property="Background" Value="{Binding TestCell,
Converter={StaticResource BGConverter}}"/>
BGConverter is a custom converter that accepts a Cell object and converts it into a Colors object. I believe I am indeed directly binding to an object that has INotify implemented, so there's no issue of nested objects. However the binding doesn't reflect changes when I click. When debugging, I found that PropertyChanged event is always null.
The closest answer I found for this is that the event will be subscribed to only if the class Cell is my datacontext. Atleast that's what I understood. How can I correct this problem?
Also I am fresh out of college, currently learning WPF on a new job, so any general recommendations are welcome too.
Thanks
Simply bind to TestCell.State instead of TestCell
I'm pretty new at this myself, but I believe your data context needs to implement INotifyPropertyChanged as well.
That is, your Board class needs to listen to the PropertyChanged event of the cells and fire its own PropertyChanged event when this happens.
I am (very) new to WPF and I have got a question regarding that. It may be a very stupid one, so please excuse me if that is the case.
I am doing a project where I am binding my textboxes, etc to static properties inside a singleton class. My problem is that the twoWay Binding is not working. When the textbox changes, I can see that the value of the property changes, but when the property changes, I can't see the textbox text changing.
To see what's going on I wrote a small app, with just the relevant code. Please find the code below.
In the code below, I am changing the text in the textbox and source property in various places and noted my observations. If someone can tell me what I am doing wrong and point me in the right direction, I would be very grateful.
I also tried INotifyPropertyChanged, but it gives problems because of the static property. Is there a different approach when implementing INotifyPropertyChanged for a static property.
Thanks in advance,
Abhi.
XAML:
<Page x:Class="TestBindingApp.Page1"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:Prefs="clr-namespace:TestBindingApp"
xmlns:cm="clr-namespace:System.ComponentModel;assembly=System"
xmlns:winForms="clr-namespace:System.Windows.Forms;assembly=System.Windows.Forms"
xmlns:wfi="clr-namespace:System.Windows.Forms.Integration;assembly=WindowsFormsIntegration"
Title="Page1" Loaded="Page_Loaded">
<Page.Resources>
<Prefs:Class1 x:Key="TClass"></Prefs:Class1>
</Page.Resources>
<Grid>
<Grid.RowDefinitions>
<RowDefinition></RowDefinition>
<RowDefinition></RowDefinition>
</Grid.RowDefinitions>
<StackPanel Orientation="Horizontal" Margin="15 5 5 0" Height="20">
<TextBlock Name="txbBankNumber" Margin="50 0 0 0" Padding="2">Bank Account Number :</TextBlock>
<TextBox Name="txtBankNumber" Margin="10 0 0 0" Width="100" MaxLength="8" HorizontalAlignment="Left">
<TextBox.Text>
<Binding Source="{StaticResource TClass}" Path="AccountNumber" Mode="TwoWay" NotifyOnValidationError="True" UpdateSourceTrigger="PropertyChanged">
</Binding>
</TextBox.Text>
</TextBox>
</StackPanel>
</Grid>
XAML.CS:
namespace TestBindingApp
{
/// <summary>
/// Interaction logic for Page1.xaml
/// </summary>
public partial class Page1 : Page
{
public Page1()
{
InitializeComponent();
txtBankNumber.Text = "ABC";
// I can see the property AccountNumber changing here
Class1.AccountNumber = "123456";
// Value in txtBankNumber doesn't change here
}
private void Page_Loaded(object sender, RoutedEventArgs e)
{
txtBankNumber.Text = "ABCDE";
// I can see the property AccountNumber changing here
Class1.AccountNumber = "12345678";
// Value in txtBankNumber doesn't change here
}
}
}
Class Class1:
using System.ComponentModel;
namespace TestBindingApp
{
public class Class1
{
// Singleton instance
private static Class1 instance;
private static string _accountNumber;
public Class1()
{
}
// Singleton instance read-only property
public static Class1 Instance
{
get
{
if (instance == null)
{
instance = new Class1();
}
return instance;
}
}
public static string AccountNumber
{
get
{
return _accountNumber;
}
set
{
if (value != _accountNumber)
{
_accountNumber = value;
}
}
}
}
}
=====================
Couldn't post my updated code in the comments, so updating my original post.
Below is my updated code, which has the "if(PropertyChanged != null)", but it gives me an error - "An object reference is required for the non-static field, method, or property 'TestBindingApp.Class1.NotifyPropertyChanged(string)'". .
I have just started learning WPF, so if you could explain in detail, that would be very helpful. Thanks for your patience.
using System.ComponentModel;
namespace TestBindingApp
{
public class Class1: INotifyPropertyChanged
{
// Singleton instance
private static Class1 instance;
private static string _accountNumber;
public event PropertyChangedEventHandler PropertyChanged;
public Class1()
{
}
// Singleton instance read-only property
public static Class1 Instance
{
get
{
if (instance == null)
{
instance = new Class1();
}
return instance;
}
}
public static string AccountNumber
{
get
{
return _accountNumber;
}
set
{
if (value != _accountNumber)
{
_accountNumber = value;
NotifyPropertyChanged("AccountNumber");
}
}
}
private void NotifyPropertyChanged(string property)
{
if (PropertyChanged != null)
PropertyChanged(this, new PropertyChangedEventArgs(property));
}
}
}
==============
Updated 23rd June, 09:53 AM UK time
Hi Arcturus, I have changed the properties to non-static, but it is still not behaving as I expect it to. Am I expecting it to do something which it isn't meant to do, or am I doing something wrong.
In the below code, I expected the textbox to show 12345678 (or maybe 123456) as the account number, but it still shows 123. In the debug mode, I can see PropertyChanged event executing correctly after each property change statement, but the value of the textbox doesn't change. Does the binding take affect only at the time of initialization (InitializeComponent()), or am I missing something here?
Page code-behind
namespace TestBindingApp
{
/// <summary>
/// Interaction logic for Page1.xaml
/// </summary>
public partial class Page1 : Page
{
public Page1()
{
Class1.Instance.AccountNumber = "123";
InitializeComponent();
Class1.Instance.AccountNumber = "123456";
}
private void Page_Loaded(object sender, RoutedEventArgs e)
{
Class1.Instance.AccountNumber = "12345678";
}
}
}
Class1.cs
namespace TestBindingApp
{
public class Class1: INotifyPropertyChanged
{
// Singleton instance
private static Class1 instance;
private static string _accountNumber;
public event PropertyChangedEventHandler PropertyChanged = delegate { };
public Class1()
{
}
// Singleton instance read-only property
public static Class1 Instance
{
get
{
if (instance == null)
{
instance = new Class1();
}
return instance;
}
}
public string AccountNumber
{
get
{
return _accountNumber;
}
set
{
if (value != _accountNumber)
{
_accountNumber = value;
OnPropertyChanged("AccountNumber");
}
}
}
private void OnPropertyChanged(string property)
{
if (PropertyChanged != null)
{
PropertyChanged(this, new PropertyChangedEventArgs(property));
}
}
}
}
You really need the INotifyPropertyChanged interface:
using System.ComponentModel;
namespace TestBindingApp
{
public class Class1
{
// Singleton instance
private static Class1 instance;
private string _accountNumber;
public Class1()
{
}
// Singleton instance read-only property
public static Class1 Instance
{
get
{
if (instance == null)
{
instance = new Class1();
}
return instance;
}
}
public string AccountNumber
{
get
{
return _accountNumber;
}
set
{
if (value != _accountNumber)
{
_accountNumber = value;
NotifyPropertyChanged("AccountNumber");
}
}
}
public event PropertyChangedEventHandler PropertyChanged;
private void NotifyPropertyChanged(string property)
{
if(PropertyChanged != null)
PropertyChanged(this, new PropertyChangedEventArgs(property));
}
}
You call NotifyPropertyChanged from a static member, but NotifyPropertyChanged itself isn't static.
Two ways to solve: Either make AccountNumber NOT static or provide an instance for your call to NotifyPropertyChanged (e.g. "Instance.NotifyPropertyChanged(...)")