Bind value from one class to another value in another class - c#

I have the following classes gist with the classes.
I want to bind Item.Visible to Items.ItemsVisible - is it possible?, if so - how?
Item.cs:
using System;
using System.ComponentModel;
namespace WpfApplication85
{
/// <summary>
/// Item Object.
/// </summary>
public class Item : INotifyPropertyChanged
{
#region INotifyPropertyChanged
public event PropertyChangedEventHandler PropertyChanged; //Event to notify when Property changed.
/// <summary>
/// Notify that Property has Changed.
/// </summary>
/// <param name="propertyName">The name of the Property</param>
protected void NotifyPropertyChanged(string propertyName)
{
if (PropertyChanged != null)
{
PropertyChanged(this, new PropertyChangedEventArgs(propertyName));
}
}
#endregion
#region Private Variables
private bool _Visible; //Bool to determine if the Item is visible or not
#endregion
#region Public Properties
//Return the value of Visible / Set the value of Visible and Notify.
public bool Visible
{
get { return _Visible; }
set
{
_Visible = value;
NotifyPropertyChanged("Visible");
}
}
#endregion
#region Constructor
/// <summary>
/// Item Constructor
/// </summary>
public Item()
{
_Visible = true;
}
#endregion
}
}
Items.cs:
using System;
using System.Collections.ObjectModel;
using System.ComponentModel;
namespace WpfApplication85
{
/// <summary>
/// Items Object.
/// </summary>
public class Items : INotifyPropertyChanged
{
#region INotifyPropertyChanged
public event PropertyChangedEventHandler PropertyChanged; //Event to notify when Property changed.
/// <summary>
/// Notify that Property has Changed.
/// </summary>
/// <param name="propertyName">The name of the Property</param>
protected void NotifyPropertyChanged(string propertyName)
{
if (PropertyChanged != null)
{
PropertyChanged(this, new PropertyChangedEventArgs(propertyName));
}
}
#endregion
#region Private Variables
private bool _itemsVisible; //Bool to determine if the Items are visible or not
private ObservableCollection<Item> _itemsCollection; //Collection of Items.
#endregion
#region Public Properties
//Return the value of ItemsVisible / Set the value of ItemsVisible and Notify.
public bool ItemsVisible
{
get { return _itemsVisible; }
set
{
_itemsVisible = value;
NotifyPropertyChanged("ItemsVisible");
}
}
//Return the Items Collection / Set the Items Collection and Notify.
public ObservableCollection<Item> ItemsCollection
{
get
{
return _itemsCollection;
}
set
{
_itemsCollection = value;
NotifyPropertyChanged("ItemsCollection");
}
}
#endregion
#region Constructor
/// <summary>
/// Items Constructor
/// </summary>
public Items()
{
_itemsVisible = true;
_itemsCollection = new ObservableCollection<Item>();
}
#endregion
#region Methods
/// <summary>
/// Add Item to the ItemsCollection.
/// </summary>
/// <param name="item">Item Object</param>
public void AddItem(Item item)
{
//Bind item.Visible to this.ItemsVisible
_itemsCollection.Add(item);
}
#endregion
}
}

Setting data binding within Items and Item properties is nothing but listening PropertyChanged or CollectionChanged event from proper interfaces.
You can use either += clause for the subscription, or WeakEventListener pattern, using the PropertyChangedEventManager and CollectionChangedEventManager
I prefer the last one, because:
Listening for events can lead to memory leaks.
So, your Items class should implement IWeakEventListener interface:
public class Items : INotifyPropertyChanged, IWeakEventListener
{
#region IWeakEventListener
public bool ReceiveWeakEvent(Type managerType, Object sender, EventArgs e)
{
if (sender == this._itemsCollection && managerType == typeof(CollectionChangedEventManager))
{
// Your collection has changed, you should add/remove
// subscription for PropertyChanged event
UpdateSubscriptions((NotifyCollectionChangedEventArgs)e);
return true;
}
if (sender is Item && managerType == typeof(PropertyChangedEventManager))
{
// The Visible property of an Item object has changed
// You should handle it properly here, for example, like this:
this.ItemsVisible = this._itemsCollection.All(i => i.Visible);
return true;
}
return false;
}
private void UpdateSubscriptions(NotifyCollectionChangedEventArgs e)
{
switch(e.Action)
{
case NotifyCollectionChangedAction.Add:
foreach (Item item in e.NewItems)
{
PropertyChangedEventManager.AddListener(item, this, "Visible");
}
break;
case NotifyCollectionChangedAction.Remove:
foreach (Item item in e.OldItems)
{
PropertyChangedEventManager.RemoveListener(item, this, "Visible");
}
break;
case NotifyCollectionChangedAction.Reset:
foreach (Item item in this._itemsCollection)
{
PropertyChangedEventManager.RemoveListener(item, this, "Visible");
PropertyChangedEventManager.AddListener(item, this, "Visible");
}
break;
default:
break;
}
}
...
public Items()
{
_itemsVisible = true;
_itemsCollection = new ObservableCollection<Item>();
CollectionChangedEventManager.AddListener(_itemsCollection, this);
}
}

Binding in the WPF sense only works on DependencyProperties, and does not apply between two standard properties (even when using INotifyPropertyChanged).
That said, if you are using these classes as View Models and are binding them to controls you could use a MultiConverter to set the visibility of the Control to collapsed when both the ItemsVisible and Visible property are true (for example).
Alternatively, you could add a Parent property to the Item class and set it to the parent Items class which would allow you to have the Item.Visible property return the parent's ItemsVisible property (or again whatever logic makes sense in your application).

Related

Pass class variable using CommandParameter in MVVM [duplicate]

This question already has an answer here:
Binding to properties in both the ViewModel and CodeBehind
(1 answer)
Closed 2 years ago.
I want to pass the ViewModel parameter(class variable) when I click the buttons in the View. When specifying a string, everything comes true, but I need to pass a non-string value from the View class(function AddNewField).
Main.xaml:
...
<Button
x:Name="ButtonAddingField"
CommandParameter="{Binding Path=Myprop}"
Command="{Binding Path=Myprop, RelativeSource={RelativeSource AncestorType=UserControl}}" />
...
Main.cs:
...
private Color myprop;
public Color Myprop
{
get => myprop;
}
...
this.DataContext = new FieldCollectionViewModel(fields); // fields - List<*my Model*>
...
FieldCollectionViewModel.cs:
...
private DelegateCommand<object> addFieldCommand;
public ICommand AddFieldCommand {
get {
if (addFieldCommand == null) addFieldCommand = new DelegateCommand<object>(AddNewField);
return addFieldCommand;
}
}
private void AddNewField(object parameter)
{
// !!! parameter = ALWAYS NULL
}
...
and my DelegateCommand.cs:
...
public class DelegateCommand<T> : ICommand
{
#region Constructors
/// <summary>
/// Constructor
/// </summary>
public DelegateCommand(Action<T> executeMethod)
: this(executeMethod, null, false)
{
}
/// <summary>
/// Constructor
/// </summary>
public DelegateCommand(Action<T> executeMethod, Func<T, bool> canExecuteMethod)
: this(executeMethod, canExecuteMethod, false)
{
}
/// <summary>
/// Constructor
/// </summary>
public DelegateCommand(Action<T> executeMethod, Func<T, bool> canExecuteMethod, bool isAutomaticRequeryDisabled)
{
if (executeMethod == null) throw new ArgumentNullException("executeMethod");
_executeMethod = executeMethod;
_canExecuteMethod = canExecuteMethod;
_isAutomaticRequeryDisabled = isAutomaticRequeryDisabled;
}
#endregion Constructors
#region Public Methods
/// <summary>
/// Method to determine if the command can be executed
/// </summary>
public bool CanExecute(T parameter)
{
if (_canExecuteMethod != null) return _canExecuteMethod(parameter);
return true;
}
/// <summary>
/// Execution of the command
/// </summary>
public void Execute(T parameter)
{
if (_executeMethod != null) _executeMethod(parameter);
}
/// <summary>
/// Raises the CanExecuteChaged event
/// </summary>
public void RaiseCanExecuteChanged()
{
OnCanExecuteChanged();
}
/// <summary>
/// Protected virtual method to raise CanExecuteChanged event
/// </summary>
protected virtual void OnCanExecuteChanged()
{
CommandManagerHelper.CallWeakReferenceHandlers(_canExecuteChangedHandlers);
}
/// <summary>
/// Property to enable or disable CommandManager's automatic requery on this command
/// </summary>
public bool IsAutomaticRequeryDisabled {
get => _isAutomaticRequeryDisabled;
set {
if (_isAutomaticRequeryDisabled != value)
{
if (value)
CommandManagerHelper.RemoveHandlersFromRequerySuggested(_canExecuteChangedHandlers);
else
CommandManagerHelper.AddHandlersToRequerySuggested(_canExecuteChangedHandlers);
_isAutomaticRequeryDisabled = value;
}
}
}
#endregion Public Methods
#region ICommand Members
/// <summary>
/// ICommand.CanExecuteChanged implementation
/// </summary>
public event EventHandler CanExecuteChanged {
add {
if (!_isAutomaticRequeryDisabled) CommandManager.RequerySuggested += value;
CommandManagerHelper.AddWeakReferenceHandler(ref _canExecuteChangedHandlers, value, 2);
}
remove {
if (!_isAutomaticRequeryDisabled) CommandManager.RequerySuggested -= value;
CommandManagerHelper.RemoveWeakReferenceHandler(_canExecuteChangedHandlers, value);
}
}
bool ICommand.CanExecute(object parameter)
{
// if T is of value type and the parameter is not
// set yet, then return false if CanExecute delegate
// exists, else return true
if (parameter == null &&
typeof(T).IsValueType)
return _canExecuteMethod == null;
return CanExecute((T)parameter);
}
void ICommand.Execute(object parameter)
{
Execute((T)parameter);
}
#endregion ICommand Members
#region Data
private readonly Action<T> _executeMethod;
private readonly Func<T, bool> _canExecuteMethod;
private bool _isAutomaticRequeryDisabled;
private List<WeakReference> _canExecuteChangedHandlers;
#endregion Data
}
...
How can I solve my problem?
UPDATE.
I changed my code, now it is working
Thanks to #mm8
Myprop must be a public property of the DataContext of the Button for your current binding to work.
If it's a property of the parent UserControl, you could bind to it using a RelativeSource:
CommandParameter="{Binding Path=Myprop, RelativeSource={RelativeSource AncestorType=UserControl}}"
Note that you can only bind to public properties. You cannot bind to fields.

Access fields of CheckBoxComboBox

I have a CheckBoxComboBox where I want to get fields of checked items so I do something like:
foreach (var currentChecked in cboDesignStatus.CheckBoxItems.Where(x => x.Checked))
{
var a = currentChecked.ComboBoxItem;
}
So var a result is :
My question is, how can I access this fields ? I want to get property Name so I try
var a = currentChecked.ComboBoxItem.Name;
But I can't it only let this:
How can I access this field
Note: variable on foreach "currentChecked" throws ComboBoxItem as ObjectSelectionWrapper
ObjectSelectionWrapper Class:
using System;
using System.Collections;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.ComponentModel;
using System.Reflection;
using System.Data;
namespace MyProject.Utility
{
/// <summary>
/// Used together with the ListSelectionWrapper in order to wrap data sources for a CheckBoxComboBox.
/// It helps to ensure you don't add an extra "Selected" property to a class that don't really need or want that information.
/// </summary>
public class ObjectSelectionWrapper<T> : INotifyPropertyChanged
{
public ObjectSelectionWrapper(T item, ListSelectionWrapper<T> container)
: base()
{
_Container = container;
_Item = item;
}
#region PRIVATE PROPERTIES
/// <summary>
/// Used as a count indicator for the item. Not necessarily displayed.
/// </summary>
private int _Count = 0;
/// <summary>
/// Is this item selected.
/// </summary>
private bool _Selected = false;
/// <summary>
/// A reference to the wrapped item.
/// </summary>
private T _Item;
/// <summary>
/// The containing list for these selections.
/// </summary>
private ListSelectionWrapper<T> _Container;
#endregion
#region PUBLIC PROPERTIES
/// <summary>
/// An indicator of how many items with the specified status is available for the current filter level.
/// Thaught this would make the app a bit more user-friendly and help not to miss items in Statusses
/// that are not often used.
/// </summary>
public int Count
{
get { return _Count; }
set { _Count = value; }
}
/// <summary>
/// A reference to the item wrapped.
/// </summary>
public T Item
{
get { return _Item; }
set { _Item = value; }
}
/// <summary>
/// The item display value. If ShowCount is true, it displays the "Name [Count]".
/// </summary>
public string Name
{
get
{
string Name = null;
if (string.IsNullOrEmpty(_Container.DisplayNameProperty))
Name = Item.ToString();
else if (Item is DataRow) // A specific implementation for DataRow
Name = ((DataRow)((Object)Item))[_Container.DisplayNameProperty].ToString();
else
{
PropertyDescriptorCollection PDs = TypeDescriptor.GetProperties(Item);
foreach (PropertyDescriptor PD in PDs)
if (PD.Name.CompareTo(_Container.DisplayNameProperty) == 0)
{
Name = (string)PD.GetValue(Item).ToString();
break;
}
if (string.IsNullOrEmpty(Name))
{
PropertyInfo PI = Item.GetType().GetProperty(_Container.DisplayNameProperty);
if (PI == null)
throw new Exception(String.Format(
"Property {0} cannot be found on {1}.",
_Container.DisplayNameProperty,
Item.GetType()));
Name = PI.GetValue(Item, null).ToString();
}
}
return _Container.ShowCounts ? String.Format("{0} [{1}]", Name, Count) : Name;
}
}
/// <summary>
/// The textbox display value. The names concatenated.
/// </summary>
public string NameConcatenated
{
get { return _Container.SelectedNames; }
}
/// <summary>
/// Indicates whether the item is selected.
/// </summary>
public bool Selected
{
get { return _Selected; }
set
{
if (_Selected != value)
{
_Selected = value;
OnPropertyChanged("Selected");
OnPropertyChanged("NameConcatenated");
}
}
}
#endregion
#region INotifyPropertyChanged
public event PropertyChangedEventHandler PropertyChanged;
protected virtual void OnPropertyChanged(string propertyName)
{
PropertyChangedEventHandler handler = PropertyChanged;
if (handler != null)
handler(this, new PropertyChangedEventArgs(propertyName));
}
#endregion
}
}
It looks like ComboBoxItem is an Object. As Nat said earlier, if you cast the object to your expected class, you can then reference the properties.
var itemName = ((Utility.ObjectSelectionWrapper<DataRow>)currentChecked.ComboBoxItem).Name;

c# DataBinding does not update Control

I'm playing with databinding on c# compact framework. I develop a simple form with a Textbox and a Label. I want to change the data binded to Textbox (bindModelTextBox) and show these changes by the Label (bindModelLabel), which is binded to the same data. Here is the code:
public partial class CreateShipment : Form {
//simple bean. Just one property: id, a string
private BasicShipmentBean toBindBasicShipment = null;
public CreateShipment() {
InitializeComponent();
BindingSource bsProva = new BindingSource();
toBindBasicShipment = new BasicShipmentBean();
toBindBasicShipment.id = "boo";
bsProva.Add(toBindBasicShipment);
bindModelLabel.DataBindings.Add("Text", bsProva, "id", true, DataSourceUpdateMode.OnPropertyChanged);
bindModelTextBox.DataBindings.Add("Text", bsProva, "id", true, DataSourceUpdateMode.OnPropertyChanged);
bindModelTextBox.LostFocus += textLoseFocus;
}
...
private void textLoseFocus(object sender, System.EventArgs e)
{
System.Diagnostics.Debug.WriteLine("focus lost. "+toBindBasicShipment.id);
}
When textbox loose focus I can see the data is updated in the bean, but, the label still shows bean's original id value. What am I missing?
You need to implement INotifyPropertyChanged on your BasicShipmentBean class. I forgot where exactly I found this originally, but here is an ObservableObject base class that implements INotifyPropertyChanged, that I use for all of my data sources.
public abstract class ObservableObject : INotifyPropertyChanged
{
#region Debugging Aides
/// <summary>
/// Warns the developer if this object does not have
/// a public property with the specified name. This
/// method does not exist in a Release build.
/// </summary>
[Conditional("DEBUG")]
[DebuggerStepThrough]
public virtual void VerifyPropertyName(string propertyName)
{
// Verify that the property name matches a real,
// public, instance property on this object.
if (TypeDescriptor.GetProperties(this)[propertyName] == null)
{
string msg = "Invalid property name: " + propertyName;
if (this.ThrowOnInvalidPropertyName)
throw new Exception(msg);
else
Debug.Fail(msg);
}
}
/// <summary>
/// Returns whether an exception is thrown, or if a Debug.Fail() is used
/// when an invalid property name is passed to the VerifyPropertyName method.
/// The default value is false, but subclasses used by unit tests might
/// override this property's getter to return true.
/// </summary>
protected virtual bool ThrowOnInvalidPropertyName { get; private set; }
#endregion // Debugging Aides
#region INotifyPropertyChanged Members
/// <summary>
/// Raises the PropertyChange event for the property specified
/// </summary>
/// <param name="propertyName">Property name to update. Is case-sensitive.</param>
public virtual void RaisePropertyChanged(string propertyName)
{
this.VerifyPropertyName(propertyName);
OnPropertyChanged(propertyName);
}
/// <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)
{
this.VerifyPropertyName(propertyName);
PropertyChangedEventHandler handler = this.PropertyChanged;
if (handler != null)
{
var e = new PropertyChangedEventArgs(propertyName);
handler(this, e);
}
}
#endregion // INotifyPropertyChanged Members
}
Then, you need to raise the OnPropertyChanged event in your setter for the id in BasicShipmentBean, e.g.:
private string _id;
public string id
{
get { return _id; }
set
{
if (value != _id)
{
_id = value;
OnPropertyChanged("id");
}
}
}
Data binding in the Compact Framework is a bit more tedious than in WPF, but much of the implementation is pretty similar.

Two Way Binding to AvalonEdit Document Text using MVVM

I want to include an AvalonEdit TextEditor control into my MVVM application. The first thing I require is to be able to bind to the TextEditor.Text property so that I can display text. To do this I have followed and example that was given in Making AvalonEdit MVVM compatible. Now, I have implemented the following class using the accepted answer as a template
public sealed class MvvmTextEditor : TextEditor, INotifyPropertyChanged
{
public static readonly DependencyProperty TextProperty =
DependencyProperty.Register("Text", typeof(string), typeof(MvvmTextEditor),
new PropertyMetadata((obj, args) =>
{
MvvmTextEditor target = (MvvmTextEditor)obj;
target.Text = (string)args.NewValue;
})
);
public new string Text
{
get { return base.Text; }
set { base.Text = value; }
}
protected override void OnTextChanged(EventArgs e)
{
RaisePropertyChanged("Text");
base.OnTextChanged(e);
}
public event PropertyChangedEventHandler PropertyChanged;
public void RaisePropertyChanged(string info)
{
if (PropertyChanged != null)
PropertyChanged(this, new PropertyChangedEventArgs(info));
}
}
Where the XAML is
<Controls:MvvmTextEditor HorizontalAlignment="Stretch"
VerticalAlignment="Stretch"
FontFamily="Consolas"
FontSize="9pt"
Margin="2,2"
Text="{Binding Text, NotifyOnSourceUpdated=True, Mode=TwoWay}"/>
Firstly, this does not work. The Binding is not shown in Snoop at all (not red, not anything, in fact I cannot even see the Text dependency property).
I have seen this question which is exactly the same as mine Two-way binding in AvalonEdit doesn't work but the accepted answer does not work (at least for me). So my question is:
How can I perform two way binding using the above method and what is the correct implementation of my MvvmTextEditor class?
Thanks for your time.
Note: I have my Text property in my ViewModel and it implements the required INotifyPropertyChanged interface.
Create a Behavior class that will attach the TextChanged event and will hook up the dependency property that is bound to the ViewModel.
AvalonTextBehavior.cs
public sealed class AvalonEditBehaviour : Behavior<TextEditor>
{
public static readonly DependencyProperty GiveMeTheTextProperty =
DependencyProperty.Register("GiveMeTheText", typeof(string), typeof(AvalonEditBehaviour),
new FrameworkPropertyMetadata(default(string), FrameworkPropertyMetadataOptions.BindsTwoWayByDefault, PropertyChangedCallback));
public string GiveMeTheText
{
get { return (string)GetValue(GiveMeTheTextProperty); }
set { SetValue(GiveMeTheTextProperty, value); }
}
protected override void OnAttached()
{
base.OnAttached();
if (AssociatedObject != null)
AssociatedObject.TextChanged += AssociatedObjectOnTextChanged;
}
protected override void OnDetaching()
{
base.OnDetaching();
if (AssociatedObject != null)
AssociatedObject.TextChanged -= AssociatedObjectOnTextChanged;
}
private void AssociatedObjectOnTextChanged(object sender, EventArgs eventArgs)
{
var textEditor = sender as TextEditor;
if (textEditor != null)
{
if (textEditor.Document != null)
GiveMeTheText = textEditor.Document.Text;
}
}
private static void PropertyChangedCallback(
DependencyObject dependencyObject,
DependencyPropertyChangedEventArgs dependencyPropertyChangedEventArgs)
{
var behavior = dependencyObject as AvalonEditBehaviour;
if (behavior.AssociatedObject!= null)
{
var editor = behavior.AssociatedObject as TextEditor;
if (editor.Document != null)
{
var caretOffset = editor.CaretOffset;
editor.Document.Text = dependencyPropertyChangedEventArgs.NewValue.ToString();
editor.CaretOffset = caretOffset;
}
}
}
}
View.xaml
<avalonedit:TextEditor
WordWrap="True"
ShowLineNumbers="True"
LineNumbersForeground="Magenta"
x:Name="textEditor"
FontFamily="Consolas"
SyntaxHighlighting="XML"
FontSize="10pt">
<i:Interaction.Behaviors>
<controls:AvalonEditBehaviour GiveMeTheText="{Binding Test, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}"/>
</i:Interaction.Behaviors>
</avalonedit:TextEditor>
i must be defined as
xmlns:i="clr-namespace:System.Windows.Interactivity;assembly=System.Windows.Interactivity"
ViewModel.cs
private string _test;
public string Test
{
get { return _test; }
set { _test = value; }
}
That should give you the Text and push it back to the ViewModel.
Create a BindableAvalonEditor class with a two-way binding on the Text property.
I was able to establish a two-way binding with the latest version of AvalonEdit by combining Jonathan Perry's answer and 123 456 789 0's answer. This allows a direct two-way binding without the need for behaviors.
Here is the source code...
public class BindableAvalonEditor : ICSharpCode.AvalonEdit.TextEditor, INotifyPropertyChanged
{
/// <summary>
/// A bindable Text property
/// </summary>
public new string Text
{
get
{
return (string)GetValue(TextProperty);
}
set
{
SetValue(TextProperty, value);
RaisePropertyChanged("Text");
}
}
/// <summary>
/// The bindable text property dependency property
/// </summary>
public static readonly DependencyProperty TextProperty =
DependencyProperty.Register(
"Text",
typeof(string),
typeof(BindableAvalonEditor),
new FrameworkPropertyMetadata
{
DefaultValue = default(string),
BindsTwoWayByDefault = true,
PropertyChangedCallback = OnDependencyPropertyChanged
}
);
protected static void OnDependencyPropertyChanged(DependencyObject obj, DependencyPropertyChangedEventArgs args)
{
var target = (BindableAvalonEditor)obj;
if (target.Document != null)
{
var caretOffset = target.CaretOffset;
var newValue = args.NewValue;
if (newValue == null)
{
newValue = "";
}
target.Document.Text = (string)newValue;
target.CaretOffset = Math.Min(caretOffset, newValue.ToString().Length);
}
}
protected override void OnTextChanged(EventArgs e)
{
if (this.Document != null)
{
Text = this.Document.Text;
}
base.OnTextChanged(e);
}
/// <summary>
/// Raises a property changed event
/// </summary>
/// <param name="property">The name of the property that updates</param>
public void RaisePropertyChanged(string property)
{
if (PropertyChanged != null)
{
PropertyChanged(this, new PropertyChangedEventArgs(property));
}
}
public event PropertyChangedEventHandler PropertyChanged;
}
I like none of these solutions. The reason the author didn't create a dependency property on Text is for performance reason. Working around it by creating an attached property means the text string must be recreated on every key stroke. On a 100mb file, this can be a serious performance issue. Internally, it only uses a document buffer and will never create the full string unless requested.
It exposes another property, Document, which is a dependency property, and it exposes the Text property to construct the string only when needed. Although you can bind to it, it would mean designing your ViewModel around a UI element which defeats the purpose of having a ViewModel UI-agnostic. I don't like that option either.
Honestly, the cleanest(ish) solution is to create 2 events in your ViewModel, one to display the text and one to update the text. Then you write a one-line event handler in your code-behind, which is fine since it's purely UI-related. That way, you construct and assign the full document string only when it's truly needed. Additionally, you don't even need to store (nor update) the text in the ViewModel. Just raise DisplayScript and UpdateScript when it is needed.
It's not an ideal solution, but there are less drawbacks than any other method I've seen.
TextBox also faces a similar issue, and it solves it by internally using a DeferredReference object that constructs the string only when it is really needed. That class is internal and not available to the public, and the Binding code is hard-coded to handle DeferredReference in a special way. Unfortunately there doesn't seen to be any way of solving the problem in the same way as TextBox -- perhaps unless TextEditor would inherit from TextBox.
Another nice OOP approach is to download the source code of AvalonEdit (it's open sourced), and creating a new class that inherits from TextEditor class (the main editor of AvalonEdit).
What you want to do is basically override the Text property and implement an INotifyPropertyChanged version of it, using dependency property for the Text property and raising the OnPropertyChanged event when text is changed (this can be done by overriding the OnTextChanged() method.
Here's a quick code (fully working) example that works for me:
public class BindableTextEditor : TextEditor, INotifyPropertyChanged
{
/// <summary>
/// A bindable Text property
/// </summary>
public new string Text
{
get { return base.Text; }
set { base.Text = value; }
}
/// <summary>
/// The bindable text property dependency property
/// </summary>
public static readonly DependencyProperty TextProperty =
DependencyProperty.Register("Text", typeof(string), typeof(BindableTextEditor), new PropertyMetadata((obj, args) =>
{
var target = (BindableTextEditor)obj;
target.Text = (string)args.NewValue;
}));
protected override void OnTextChanged(EventArgs e)
{
RaisePropertyChanged("Text");
base.OnTextChanged(e);
}
/// <summary>
/// Raises a property changed event
/// </summary>
/// <param name="property">The name of the property that updates</param>
public void RaisePropertyChanged(string property)
{
if (PropertyChanged != null)
{
PropertyChanged(this, new PropertyChangedEventArgs(property));
}
}
public event PropertyChangedEventHandler PropertyChanged;
}
For those wondering about an MVVM implementation using AvalonEdit, here is one of the ways it can be done, first we have the class
/// <summary>
/// Class that inherits from the AvalonEdit TextEditor control to
/// enable MVVM interaction.
/// </summary>
public class CodeEditor : TextEditor, INotifyPropertyChanged
{
// Vars.
private static bool canScroll = true;
/// <summary>
/// Default constructor to set up event handlers.
/// </summary>
public CodeEditor()
{
// Default options.
FontSize = 12;
FontFamily = new FontFamily("Consolas");
Options = new TextEditorOptions
{
IndentationSize = 3,
ConvertTabsToSpaces = true
};
}
#region Text.
/// <summary>
/// Dependancy property for the editor text property binding.
/// </summary>
public static readonly DependencyProperty TextProperty =
DependencyProperty.Register("Text", typeof(string), typeof(CodeEditor),
new PropertyMetadata((obj, args) =>
{
CodeEditor target = (CodeEditor)obj;
target.Text = (string)args.NewValue;
}));
/// <summary>
/// Provide access to the Text.
/// </summary>
public new string Text
{
get { return base.Text; }
set { base.Text = value; }
}
/// <summary>
/// Return the current text length.
/// </summary>
public int Length
{
get { return base.Text.Length; }
}
/// <summary>
/// Override of OnTextChanged event.
/// </summary>
protected override void OnTextChanged(EventArgs e)
{
RaisePropertyChanged("Length");
base.OnTextChanged(e);
}
/// <summary>
/// Event handler to update properties based upon the selection changed event.
/// </summary>
void TextArea_SelectionChanged(object sender, EventArgs e)
{
this.SelectionStart = SelectionStart;
this.SelectionLength = SelectionLength;
}
/// <summary>
/// Event that handles when the caret changes.
/// </summary>
void TextArea_CaretPositionChanged(object sender, EventArgs e)
{
try
{
canScroll = false;
this.TextLocation = TextLocation;
}
finally
{
canScroll = true;
}
}
#endregion // Text.
#region Caret Offset.
/// <summary>
/// DependencyProperty for the TextEditorCaretOffset binding.
/// </summary>
public static DependencyProperty CaretOffsetProperty =
DependencyProperty.Register("CaretOffset", typeof(int), typeof(CodeEditor),
new PropertyMetadata((obj, args) =>
{
CodeEditor target = (CodeEditor)obj;
if (target.CaretOffset != (int)args.NewValue)
target.CaretOffset = (int)args.NewValue;
}));
/// <summary>
/// Access to the SelectionStart property.
/// </summary>
public new int CaretOffset
{
get { return base.CaretOffset; }
set { SetValue(CaretOffsetProperty, value); }
}
#endregion // Caret Offset.
#region Selection.
/// <summary>
/// DependencyProperty for the TextLocation. Setting this value
/// will scroll the TextEditor to the desired TextLocation.
/// </summary>
public static readonly DependencyProperty TextLocationProperty =
DependencyProperty.Register("TextLocation", typeof(TextLocation), typeof(CodeEditor),
new PropertyMetadata((obj, args) =>
{
CodeEditor target = (CodeEditor)obj;
TextLocation loc = (TextLocation)args.NewValue;
if (canScroll)
target.ScrollTo(loc.Line, loc.Column);
}));
/// <summary>
/// Get or set the TextLocation. Setting will scroll to that location.
/// </summary>
public TextLocation TextLocation
{
get { return base.Document.GetLocation(SelectionStart); }
set { SetValue(TextLocationProperty, value); }
}
/// <summary>
/// DependencyProperty for the TextEditor SelectionLength property.
/// </summary>
public static readonly DependencyProperty SelectionLengthProperty =
DependencyProperty.Register("SelectionLength", typeof(int), typeof(CodeEditor),
new PropertyMetadata((obj, args) =>
{
CodeEditor target = (CodeEditor)obj;
if (target.SelectionLength != (int)args.NewValue)
{
target.SelectionLength = (int)args.NewValue;
target.Select(target.SelectionStart, (int)args.NewValue);
}
}));
/// <summary>
/// Access to the SelectionLength property.
/// </summary>
public new int SelectionLength
{
get { return base.SelectionLength; }
set { SetValue(SelectionLengthProperty, value); }
}
/// <summary>
/// DependencyProperty for the TextEditor SelectionStart property.
/// </summary>
public static readonly DependencyProperty SelectionStartProperty =
DependencyProperty.Register("SelectionStart", typeof(int), typeof(CodeEditor),
new PropertyMetadata((obj, args) =>
{
CodeEditor target = (CodeEditor)obj;
if (target.SelectionStart != (int)args.NewValue)
{
target.SelectionStart = (int)args.NewValue;
target.Select((int)args.NewValue, target.SelectionLength);
}
}));
/// <summary>
/// Access to the SelectionStart property.
/// </summary>
public new int SelectionStart
{
get { return base.SelectionStart; }
set { SetValue(SelectionStartProperty, value); }
}
#endregion // Selection.
#region Properties.
/// <summary>
/// The currently loaded file name. This is bound to the ViewModel
/// consuming the editor control.
/// </summary>
public string FilePath
{
get { return (string)GetValue(FilePathProperty); }
set { SetValue(FilePathProperty, value); }
}
// Using a DependencyProperty as the backing store for FilePath.
// This enables animation, styling, binding, etc...
public static readonly DependencyProperty FilePathProperty =
DependencyProperty.Register("FilePath", typeof(string), typeof(CodeEditor),
new PropertyMetadata(String.Empty, OnFilePathChanged));
#endregion // Properties.
#region Raise Property Changed.
/// <summary>
/// Implement the INotifyPropertyChanged event handler.
/// </summary>
public event PropertyChangedEventHandler PropertyChanged;
public void RaisePropertyChanged([CallerMemberName] string caller = null)
{
var handler = PropertyChanged;
if (handler != null)
PropertyChanged(this, new PropertyChangedEventArgs(caller));
}
#endregion // Raise Property Changed.
}
Then in your view where you want to have AvalonEdit, you can do
...
<Grid>
<Local:CodeEditor
x:Name="CodeEditor"
FilePath="{Binding FilePath,
Mode=TwoWay,
NotifyOnSourceUpdated=True,
NotifyOnTargetUpdated=True}"
WordWrap="{Binding WordWrap,
Mode=TwoWay,
NotifyOnSourceUpdated=True,
NotifyOnTargetUpdated=True}"
ShowLineNumbers="{Binding ShowLineNumbers,
Mode=TwoWay,
NotifyOnSourceUpdated=True,
NotifyOnTargetUpdated=True}"
SelectionLength="{Binding SelectionLength,
Mode=TwoWay,
NotifyOnSourceUpdated=True,
NotifyOnTargetUpdated=True}"
SelectionStart="{Binding SelectionStart,
Mode=TwoWay,
NotifyOnSourceUpdated=True,
NotifyOnTargetUpdated=True}"
TextLocation="{Binding TextLocation,
Mode=TwoWay,
NotifyOnSourceUpdated=True,
NotifyOnTargetUpdated=True}"/>
</Grid>
Where this can be placed in a UserControl or Window or what ever, then in the ViewModel for this view we have (where I am using Caliburn Micro for the MVVM framework stuff)
public string FilePath
{
get { return filePath; }
set
{
if (filePath == value)
return;
filePath = value;
NotifyOfPropertyChange(() => FilePath);
}
}
/// <summary>
/// Should wrap?
/// </summary>
public bool WordWrap
{
get { return wordWrap; }
set
{
if (wordWrap == value)
return;
wordWrap = value;
NotifyOfPropertyChange(() => WordWrap);
}
}
/// <summary>
/// Display line numbers?
/// </summary>
public bool ShowLineNumbers
{
get { return showLineNumbers; }
set
{
if (showLineNumbers == value)
return;
showLineNumbers = value;
NotifyOfPropertyChange(() => ShowLineNumbers);
}
}
/// <summary>
/// Hold the start of the currently selected text.
/// </summary>
private int selectionStart = 0;
public int SelectionStart
{
get { return selectionStart; }
set
{
selectionStart = value;
NotifyOfPropertyChange(() => SelectionStart);
}
}
/// <summary>
/// Hold the selection length of the currently selected text.
/// </summary>
private int selectionLength = 0;
public int SelectionLength
{
get { return selectionLength; }
set
{
selectionLength = value;
UpdateStatusBar();
NotifyOfPropertyChange(() => SelectionLength);
}
}
/// <summary>
/// Gets or sets the TextLocation of the current editor control. If the
/// user is setting this value it will scroll the TextLocation into view.
/// </summary>
private TextLocation textLocation = new TextLocation(0, 0);
public TextLocation TextLocation
{
get { return textLocation; }
set
{
textLocation = value;
UpdateStatusBar();
NotifyOfPropertyChange(() => TextLocation);
}
}
And that's it! Done.
I hope this helps.
Edit. for all those looking for an example of working with AvalonEdit using MVVM, you can download a very basic editor application from http://1drv.ms/1E5nhCJ.
Notes. This application actually creates a MVVM friendly editor control by inheriting from the AvalonEdit standard control and adds additional Dependency Properties to it as appropriate - *this is different to what I have shown in the answer given above*. However, in the solution I have also shown how this can be done (as I describe in the answer above) using Attached Properties and there is code in the solution under the Behaviors namespace. What is actually implemented however, is the first of the above approaches.
Please also be aware that there is some code in the solution that is unused. This *sample* was a stripped back version of a larger application and I have left some code in as it could be useful to the user who downloads this example editor. In addition to the above, in the example code I access the Text by binding to document, there are some purest that may argue that this is not pure-MVVM, and I say "okay, but it works". Some times fighting this pattern is not the way to go.
I hope this of use to some of you.

WPF MVVM - How to detect if a View is "Dirty"

I currently have a requirement to notify my application user if any fields have been changed/updated on a View.
For example, if the user changes a date field on the View and then tries to close the View, the application would display a message asking the user to Continue and lose changes or Cancel so that they can click the Save button.
Problem is: How do I detect that any of the data fields changed in the View?
Hope this makes sense, than you in advance, regards,
One approach you can take is to leverage the IChangeTracking and INotifyPropertyChanged interfaces.
If you create an abstract base class that your view models inherit from (ViewModelBase) which implements the IChangeTracking and INotifyPropertyChanged interfaces, you can have your view model base attach to notification of property changes (in effect signaling that the view model has been modified) and which will set the IsChanged property to true to indicate that the view model is 'dirty'.
Using this approach, you are relying on property change notification via data binding to track changes and would reset the change tracking after any commits are made.
In the case you described you could handle the Unloaded or Closing event of your view to inspect the DataContext; and if the DataContext implements IChangeTracking you can use the IsChanged property to determine if any unaccepted changes have been made.
Simple example:
/// <summary>
/// Provides a base class for objects that support property change notification
/// and querying for changes and resetting of the changed status.
/// </summary>
public abstract class ViewModelBase : IChangeTracking, INotifyPropertyChanged
{
//========================================================
// Constructors
//========================================================
#region ViewModelBase()
/// <summary>
/// Initializes a new instance of the <see cref="ViewModelBase"/> class.
/// </summary>
protected ViewModelBase()
{
this.PropertyChanged += new PropertyChangedEventHandler(OnNotifiedOfPropertyChanged);
}
#endregion
//========================================================
// Private Methods
//========================================================
#region OnNotifiedOfPropertyChanged(object sender, PropertyChangedEventArgs e)
/// <summary>
/// Handles the <see cref="INotifyPropertyChanged.PropertyChanged"/> event for this object.
/// </summary>
/// <param name="sender">The source of the event.</param>
/// <param name="e">A <see cref="PropertyChangedEventArgs"/> that contains the event data.</param>
private void OnNotifiedOfPropertyChanged(object sender, PropertyChangedEventArgs e)
{
if (e != null && !String.Equals(e.PropertyName, "IsChanged", StringComparison.Ordinal))
{
this.IsChanged = true;
}
}
#endregion
//========================================================
// IChangeTracking Implementation
//========================================================
#region IsChanged
/// <summary>
/// Gets the object's changed status.
/// </summary>
/// <value>
/// <see langword="true"/> if the object’s content has changed since the last call to <see cref="AcceptChanges()"/>; otherwise, <see langword="false"/>.
/// The initial value is <see langword="false"/>.
/// </value>
public bool IsChanged
{
get
{
lock (_notifyingObjectIsChangedSyncRoot)
{
return _notifyingObjectIsChanged;
}
}
protected set
{
lock (_notifyingObjectIsChangedSyncRoot)
{
if (!Boolean.Equals(_notifyingObjectIsChanged, value))
{
_notifyingObjectIsChanged = value;
this.OnPropertyChanged("IsChanged");
}
}
}
}
private bool _notifyingObjectIsChanged;
private readonly object _notifyingObjectIsChangedSyncRoot = new Object();
#endregion
#region AcceptChanges()
/// <summary>
/// Resets the object’s state to unchanged by accepting the modifications.
/// </summary>
public void AcceptChanges()
{
this.IsChanged = false;
}
#endregion
//========================================================
// INotifyPropertyChanged Implementation
//========================================================
#region PropertyChanged
/// <summary>
/// Occurs when a property value changes.
/// </summary>
public event PropertyChangedEventHandler PropertyChanged;
#endregion
#region OnPropertyChanged(PropertyChangedEventArgs e)
/// <summary>
/// Raises the <see cref="INotifyPropertyChanged.PropertyChanged"/> event.
/// </summary>
/// <param name="e">A <see cref="PropertyChangedEventArgs"/> that provides data for the event.</param>
protected void OnPropertyChanged(PropertyChangedEventArgs e)
{
var handler = this.PropertyChanged;
if (handler != null)
{
handler(this, e);
}
}
#endregion
#region OnPropertyChanged(string propertyName)
/// <summary>
/// Raises the <see cref="INotifyPropertyChanged.PropertyChanged"/> event for the specified <paramref name="propertyName"/>.
/// </summary>
/// <param name="propertyName">The <see cref="MemberInfo.Name"/> of the property whose value has changed.</param>
protected void OnPropertyChanged(string propertyName)
{
this.OnPropertyChanged(new PropertyChangedEventArgs(propertyName));
}
#endregion
#region OnPropertyChanged(params string[] propertyNames)
/// <summary>
/// Raises the <see cref="INotifyPropertyChanged.PropertyChanged"/> event for the specified <paramref name="propertyNames"/>.
/// </summary>
/// <param name="propertyNames">An <see cref="Array"/> of <see cref="String"/> objects that contains the names of the properties whose values have changed.</param>
/// <exception cref="ArgumentNullException">The <paramref name="propertyNames"/> is a <see langword="null"/> reference (Nothing in Visual Basic).</exception>
protected void OnPropertyChanged(params string[] propertyNames)
{
if (propertyNames == null)
{
throw new ArgumentNullException("propertyNames");
}
foreach (var propertyName in propertyNames)
{
this.OnPropertyChanged(propertyName);
}
}
#endregion
}
In MVVM a View is binded to a View-Model which in turn is binded to a Model.
The view can not be dirty, since it's changes are reflected immediately to the View-Model.
If you want changes to be applied to Model only on "OK" or "Accept",
bind View to a View-Model that doesn't apply changes to Model,
until an ApplyCommand or AcceptCommand (that you define and implement) is executed.
(The commands that the View is binded to are implemented by the View-Model.)
Example - VM:
public class MyVM : INotifyPropertyChanged
{
public string MyText
{
get
{
return _MyText;
}
set
{
if (value == _MyText)
return;
_MyText = value;
NotifyPropertyChanged("MyText");
}
}
private string _MyText;
public string MyTextTemp
{
get
{
return _MyTextTemp;
}
set
{
if (value == _MyTextTemp)
return;
_MyTextTemp = value;
NotifyPropertyChanged("MyTextTemp");
NotifyPropertyChanged("IsTextDirty");
}
}
private string _MyTextTemp;
public bool IsTextDirty
{
get
{
return MyText != MyTextTemp;
}
}
public bool IsMyTextBeingEdited
{
get
{
return _IsMyTextBeingEdited;
}
set
{
if (value == _IsMyTextBeingEdited)
return;
_IsMyTextBeingEdited = value;
if (!value)
{
MyText = MyTextTemp;
}
NotifyPropertyChanged("IsMyTextBeingEdited");
}
}
private bool _IsMyTextBeingEdited;
public event PropertyChangedEventHandler PropertyChanged;
protected void NotifyPropertyChanged(string propertyName)
{
if (PropertyChanged != null)
PropertyChanged(this, new PropertyChangedEventArgs(propertyName));
}
}
Example - View:
<Label Content="{Binding MyText}" />
<!-- You can translate the events to commands by using a suitable framework -->
<!-- or use code behind to update a new dependency property as in this example -->
<TextBox
LostFocus="TextBox_LostFocus"
GotFocus="TextBox_GotFocus"
Text="{Binding Path=MyTextTemp, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}"
/>
Example - view - code behind:
public MainWindow()
{
InitializeComponent();
SetBinding(IsTextBoxFocusedProperty,
new Binding
{
Path = new PropertyPath("IsMyTextBeingEdited"),
Mode = BindingMode.OneWayToSource,
});
}
private void TextBox_LostFocus(object sender, RoutedEventArgs e)
{
IsTextBoxFocused = false;
}
private void TextBox_GotFocus(object sender, RoutedEventArgs e)
{
IsTextBoxFocused = true;
}
#region IsTextBoxFocused
/// <summary>
/// Gets or Sets IsTextBoxFocused
/// </summary>
public bool IsTextBoxFocused
{
get
{
return (bool)this.GetValue(IsTextBoxFocusedProperty);
}
set
{
this.SetValue(IsTextBoxFocusedProperty, value);
}
}
/// <summary>
/// The backing DependencyProperty behind IsTextBoxFocused
/// </summary>
public static readonly DependencyProperty IsTextBoxFocusedProperty = DependencyProperty.Register(
"IsTextBoxFocused", typeof(bool), typeof(MainWindow), new PropertyMetadata(default(bool)));
#endregion
Idea:  check  entitystate:
Problem is that this refers to the whole VIEW, so when a new participant (refreshes form) is selected before any editing,  the value is also "Modified". After a save, if nothing else changes and we don’t switch participants, the value is  "Unchanged" 
 
 

Categories