windowsforms-113956-cross-platform-app-development-winforms-mvvm-concepts-data-bindings-and-notifications.md
Depending on what properties you bind, there are three possible scenarios:
You can bind a UI control to a property in a ViewModel using standard .NET mechanisms or the DevExpress MVVM Framework. We recommend using the MvvmContext.SetBinding method. This method uses a strongly typed, fluent API that simplifies code, reduces the chance of errors, and works with other DevExpress MVVM features (such as property tracking and command binding).
A view contains a LabelControl with no initial text. A ViewModel exposes a bindable LabelText property. You can bind this property to the control’s Text property in either of the following ways:
[POCOViewModel()]
public class Form1ViewModel {
public Form1ViewModel() {
LabelText = "Value stored in ViewModel";
}
public virtual string LabelText { get; set; }
}
<POCOViewModel()>
Public Class Form1ViewModel
Public Sub New()
LabelText = "Value stored in ViewModel"
End Sub
Public Overridable Property LabelText() As String
End Class
Option 1 (Recommended) – Use MvvmContext.SetBinding.
var fluent = mvvmContext1.OfType<Form1ViewModel>();
fluent.SetBinding(labelControl1, l => l.Text, x=>x.LabelText);
Dim fluent = mvvmContext1.OfType(Of Form1ViewModel)()
fluent.SetBinding(labelControl1, Function(l) l.Text, Function(x) x.LabelText)
Option 2 – Use standard DataBindings.
Form1ViewModel viewModel = mvvmContext1.GetViewModel<Form1ViewModel>();
// This method works but uses string-based property names, which are harder to maintain.
labelControl1.DataBindings.Add("Text", viewModel, "LabelText");
Dim viewModel As Form1ViewModel = mvvmContext1.GetViewModel(Of Form1ViewModel)()
' This method works but uses string-based property names, which are harder to maintain.
labelControl1.DataBindings.Add("Text", viewModel, "LabelText")
Tip
To bind a data editor to a property and choose a specific update mode, use standard data bindings instead of the SetBinding method (see Data Binding).
If a bound property’s value can change, it is important to notify a related property about this change. To do this, send update notifications to dependent properties. If you use POCO ViewModels , the DevExpress Framework can send these notifications.
What is a POCO ViewModel?
In MVVM applications, every View has a related ViewModel. When you use the DevExpress MVVM Framework, you should add an MvvmContext component to each View and point this component to a ViewModel related to this View. We recommend doing this at design time via the component’s smart tag menu.
You can also use the ViewModelType property to do this in code.
mvvmContext.ViewModelType = typeof(ViewModel);
mvvmContext.ViewModelType = GetType(ViewModel)
The Framework considers every ViewModel assigned to the MvvmContext component as a POCO (Plain Old CRL Object) ViewModel. POCO ViewModels have a number of naming and syntax conventions. If you follow them, the Framework predicts what is that you want to do and acts accordingly. For instance, update notifications are automatically sent to (from) “correctly” declared properties.
Create a public virtual auto implemented property to allow the Framework send update notifications to and from this property. You can also declare the property setter as protected.
public virtual string Name { get; set; }
public virtual int ID { get; protected set; }
Public Overridable Property Name() As String
Public Overridable Property ID() As Integer
Get
Return _privateID
End Get
Protected Set(ByVal value As Integer)
_privateID = value
End Set
End Property
Note
The Framework ignores properties with a backing field. To be able to bind such properties, decorate them with the DevExpress.Mvvm.DataAnnotations.BindableProperty attribute.
using DevExpress.Mvvm.DataAnnotations;
//. . .
string name;
[BindableProperty]
public virtual string Name {
get { return name; }
set { name = value; }
}
Imports DevExpress.Mvvm.DataAnnotations
'. . .
Private name_field As String
<BindableProperty>
Public Overridable Property Name() As String
Get
Return name_field
End Get
Set(ByVal value As String)
name_field = value
End Set
End Property
If you want a specific method to be called each time such property is updated, specify this method’s name in the same attribute.
[BindableProperty(OnPropertyChangedMethodName= "OnLookUpEdit1ValueChanged")]
public virtual string Name {
// ...
}
<BindableProperty(OnPropertyChangedMethodName:= "OnLookUpEdit1ValueChanged")>
Public Overridable ReadOnly Property Name() As String
' ...
End Property
In the Bindable Properties demo, a Label displays the TextEdit editor’s value. The TextEdit is bound to the auto implemented virtual Text property (stores raw editor value) and the Label is bound to Title (stores formatted “Text” value).
Since the “Text” property follows POCO naming conventions, the TextEdit-to-Text binding is two-way : an editor updates its value when the ViewModel property changes, and the ViewModel property updates its value when a user modifies the editor text. The Label-to-Title binding is one-way because the “Title” property has no public set method. In this setup, we do not need a two-way binding for “Title” because users cannot change the Label text.
//View code
var fluent = mvvmContext.OfType<ViewModel>();
fluent.SetBinding(editor, ed => ed.EditValue, x => x.Text);
fluent.SetBinding(label, lbl => lbl.Text, x => x.Title);
//ViewModel code
public class ViewModel {
public virtual string Text { get; set; }
public string Title {
get {
if(Text == null)
return "Title: (Null)";
if(Text.Length == 0)
return "Title: (Empty)";
if(string.IsNullOrWhiteSpace(Text))
return "Title: (Whitespace)";
return "Title: " + Text;
}
}
}
'View code
Dim fluent = mvvmContext.OfType(Of ViewModel)()
fluent.SetBinding(editor, Function(ed) ed.EditValue, Function(x) x.Text)
fluent.SetBinding(label, Function(lbl) lbl.Text, Function(x) x.Title)
'ViewModel code
Public Class ViewModel
Public Overridable Property Text() As String
Public ReadOnly Property Title() As String
Get
If Text Is Nothing Then
Return "Title: (Null)"
End If
If Text.Length = 0 Then
Return "Title: (Empty)"
End If
If String.IsNullOrWhiteSpace(Text) Then
Return "Title: (Whitespace)"
End If
Return "Title: " & Text
End Get
End Property
End Class
Important
The code above demonstrates the difference between “Title” and “Text” properties and is not complete. The demo module also uses property dependencies to update “Title” when “Text” changes; run the demo to view the complete code.
If you need to bind a nested ViewModel property, use the DevExpress.Mvvm.POCO.ViewModelSource.Create method to create an instance of this nested ViewModel that you can access through the parent ViewModel. The View binding syntax uses the same SetBinding method.
//Nested ViewModel
public class NestedViewModel {
public virtual string Text { get; set; }
}
//Parent ViewModel
public class ViewModelWithChild {
public ViewModelWithChild() {
Child = ViewModelSource.Create<NestedViewModel>();
}
public NestedViewModel Child {
get;
private set;
}
}
//View code
var fluent = mvvmContext.OfType<ViewModelWithChild>();
fluent.SetBinding(editor, ed => ed.EditValue, x => x.Child.Text);
'Nested ViewModel
Public Class NestedViewModel
Public Overridable Property Text() As String
End Class
'Parent ViewModel
Public Class ViewModelWithChild
Public Sub New()
Child = ViewModelSource.Create(Of NestedViewModel)()
End Sub
Private privateChild As NestedViewModel
Public Property Child() As NestedViewModel
Get
Return privateChild
End Get
Private Set(ByVal value As NestedViewModel)
privateChild = value
End Set
End Property
End Class
'View code
Dim fluent = mvvmContext.OfType(Of ViewModelWithChild)()
fluent.SetBinding(editor, Function(ed) ed.EditValue, Function(x) x.Child.Text)
If you do not use POCO models, the Framework does not send update notifications automatically. To send notifications in this case, implement the INotifyPropertyChanged interface or create -PropertyName-Changed events. Note that you cannot use the mvvmContext.ViewModelType property, and you should call the mvvmContext.SetViewModel method to pass a ViewModel instance to the component.
//ViewModel code
public class ObjectWithTextAndTitle {
string textCore;
public string Text {
get { return textCore; }
set {
if(textCore == value) return;
textCore = value;
OnTextChanged();
}
}
protected virtual void OnTextChanged() {
RaiseTextChanged();
}
protected void RaiseTextChanged() {
var handler = TextChanged;
if(handler != null) handler(this, EventArgs.Empty);
}
public event EventHandler TextChanged;
}
//View code
mvvmContext.SetViewModel(typeof(ObjectWithTextAndTitle), viewModelInstance);
var fluent = mvvmContext.OfType<ObjectWithTextAndTitle>();
fluent.SetBinding(editor, ed => ed.EditValue, x => x.Text);
'ViewModel code
Public Class ObjectWithTextAndTitle
Private textCore As String
Public Property Text() As String
Get
Return textCore
End Get
Set(ByVal value As String)
If textCore = value Then
Return
End If
textCore = value
OnTextChanged()
End Set
End Property
Protected Overridable Sub OnTextChanged()
RaiseTextChanged()
End Sub
Protected Sub RaiseTextChanged()
Dim handler = TextChangedEvent
If handler IsNot Nothing Then
handler(Me, EventArgs.Empty)
End If
End Sub
Public Event TextChanged As EventHandler
End Class
'View code
mvvmContext.SetViewModel(GetType(ObjectWithTextAndTitle), viewModelInstance)
Dim fluent = mvvmContext.OfType(Of ObjectWithTextAndTitle)()
fluent.SetBinding(editor, Function(ed) ed.EditValue, Function(x) x.Text)
To bind an editor to a Model property, add a System.Windows.Forms.BindingSource to a View and use standard DataBindings API. The optional updateMode parameter allows you to specify if the property updates its value when an editor value changes, and (if yes) whether it should happen immediately or when the editor is validated.
editor.DataBindings.Add(...);
editor.DataBindings.Add(...)
The Binding of Entity Properties demo defines a custom Entity class. Instances of this class are used as data records and have ID and Text fields. Both data fields are bound to editors, and the BindingSource component stores the active Entity object.
//View
mvvmContext.ViewModelType = typeof(ViewModel);
var fluentApi = mvvmContext.OfType<ViewModel>();
// Create a BindingSource and populate it with a data object.
//When a user modifies this object, the "Update" method is called
BindingSource entityBindingSource = new BindingSource();
entityBindingSource.DataSource = typeof(Entity);
fluentApi.SetObjectDataSourceBinding(entityBindingSource, x => x.Entity, x => x.Update());
// Data Bindings
idEditor.DataBindings.Add(
new Binding("EditValue", entityBindingSource, "ID"));
textEditor.DataBindings.Add(
new Binding("EditValue", entityBindingSource, "Text", true, DataSourceUpdateMode.OnPropertyChanged));
//ViewModel
public class ViewModel {
//...
public virtual Entity Entity {
get;
set;
}
//...
}
//Model
public class Entity {
public Entity(int id) {
this.ID = id;
this.Text = "Entity " + id.ToString();
}
public int ID { get; private set; }
public string Text { get; set; }
}
'View
mvvmContext.ViewModelType = GetType(ViewModel)
Dim fluentApi = mvvmContext.OfType(Of ViewModel)()
' Create a BindingSource and populate it with a data object.
'When a user modifies this object, the "Update" method is called
Dim entityBindingSource As New BindingSource()
entityBindingSource.DataSource = GetType(Entity)
fluentApi.SetObjectDataSourceBinding(entityBindingSource, Function(x) x.Entity, Function(x) x.Update())
' Data Bindings
idEditor.DataBindings.Add(New Binding("EditValue", entityBindingSource, "ID"))
textEditor.DataBindings.Add(New Binding("EditValue", entityBindingSource, "Text", True, DataSourceUpdateMode.OnPropertyChanged))
'ViewModel
Public Class ViewModel
'...
Public Overridable Property Entity() As Entity
'...
End Class
'Model
Public Class Entity
Public Sub New(ByVal id As Integer)
Me.ID = id
Me.Text = "Entity " & id.ToString()
End Sub
Private privateID As Integer
Public Property ID() As Integer
Get
Return privateID
End Get
Private Set(ByVal value As Integer)
privateID = value
End Set
End Property
Public Property Text() As String
End Class
You can use the SetBinding method as well…
fluent.SetBinding(idEditor, l => l.EditValue, x => x.Entity.ID);
fluent.SetBinding(textEditor, l => l.EditValue, x => x.Entity.Text);
fluent.SetBinding(idEditor, Function(te) te.EditValue, Function(dl) dl.Entity.ID)
fluent.SetBinding(textEditor, Function(te) te.EditValue, Function(dl) dl.Entity.Text)
…but in this case you lose the option to set a required DataSourceUpdateMode, which allows you to prevent excessive update notifications.
A property dependency is a relation between two properties from the same ViewModel. When one property changes, another updates its value.
In the “MVVM Best Practices” demo, multiple modules demonstrate the following setup:
The code that binds View elements to ViewModel properties is identical for every demo module that uses the sample UI.
mvvmContext.ViewModelType = typeof(MultViewModel);
var fluentAPI = mvvmContext.OfType<MultViewModel>();
fluentAPI.SetBinding(editor1, e => e.EditValue, x => x.Operand1);
fluentAPI.SetBinding(editor2, e => e.EditValue, x => x.Operand2);
fluentAPI.SetBinding(resultLabel, l => l.Text, x => x.ResultText);
mvvmContext.ViewModelType = GetType(MultViewModel)
Dim fluentAPI = mvvmContext.OfType(Of MultViewModel)()
fluentAPI.SetBinding(editor1, Sub(e) e.EditValue, Sub(x) x.Operand1)
fluentAPI.SetBinding(editor2, Sub(e) e.EditValue, Sub(x) x.Operand2)
fluentAPI.SetBinding(resultLabel, Sub(l) l.Text, Sub(x) x.ResultText)
However, property dependencies are declared differently in every module.
In POCO ViewModels, you can declare OnXChanged methods where X is a property name. The Framework calls these methods when related properties’ values change.
public class MultViewModel {
public virtual int Operand1 { get; set; }
public virtual int Operand2 { get; set; }
public virtual int Result { get; set; }
public virtual string ResultText { get; set; }
protected void OnOperand1Changed() {
UpdateResult();
}
protected void OnOperand2Changed() {
UpdateResult();
}
protected void OnResultChanged() {
UpdateResultText();
}
void UpdateResult() {
Result = Operand1 * Operand2;
}
void UpdateResultText() {
ResultText = string.Format("The result is: {0:n0}", Result);
}
}
Public Class MultViewModel
Public Overridable Property Operand1() As Integer
Public Overridable Property Operand2() As Integer
Public Overridable Property Result() As Integer
Public Overridable Property ResultText() As String
Protected Sub OnOperand1Changed()
UpdateResult()
End Sub
Protected Sub OnOperand2Changed()
UpdateResult()
End Sub
Protected Sub OnResultChanged()
UpdateResultText()
End Sub
Private Sub UpdateResult()
Result = Operand1 * Operand2
End Sub
Private Sub UpdateResultText()
ResultText = String.Format("The result is: {0:n0}", Result)
End Sub
End Class
If your update method is not called “On…Changed”, use the DevExpress.Mvvm.DataAnnotations.BindableProperty attribute to tell the Framework it should call this method when a property value changes. In the following code snippet, DevExpress.Mvvm.POCO.RaisePropertyChanged is a DevExpress extension method that sends update notifications to dependent properties.
public class SumViewModel {
[BindableProperty(OnPropertyChangedMethodName = "NotifyResultAndResultTextChanged")]
public virtual int Operand1 { get; set; }
[BindableProperty(OnPropertyChangedMethodName = "NotifyResultAndResultTextChanged")]
public virtual int Operand2 { get; set; }
public int Result {
get { return Operand1 + Operand2; }
}
public string ResultText {
get { return string.Format("The result is: {0:n0}", Result); }
}
protected void NotifyResultAndResultTextChanged() {
this.RaisePropertyChanged(x => x.Result);
this.RaisePropertyChanged(x => x.ResultText);
}
}
Public Class SumViewModel
<BindableProperty(OnPropertyChangedMethodName := "NotifyResultAndResultTextChanged")>
Public Overridable Property Operand1() As Integer
<BindableProperty(OnPropertyChangedMethodName := "NotifyResultAndResultTextChanged")>
Public Overridable Property Operand2() As Integer
Public ReadOnly Property Result() As Integer
Get
Return Operand1 + Operand2
End Get
End Property
Public ReadOnly Property ResultText() As String
Get
Return String.Format("The result is: {0:n0}", Result)
End Get
End Property
Protected Sub NotifyResultAndResultTextChanged()
Me.RaisePropertyChanged(Function(x) x.Result)
Me.RaisePropertyChanged(Function(x) x.ResultText)
End Sub
End Class
Label dependent properties with the DevExpress.Mvvm.DataAnnotations.DependsOnProperties attribute. Note that unlike in previous examples, the code below uses one dependency only: “ResultText” depends on both “Operand” properties. You cannot create chained dependencies with this attribute.
public class MultViewModelEx {
public virtual int Operand1 { get; set; }
public virtual int Operand2 { get; set; }
[DependsOnProperties("Operand1", "Operand2")]
public string ResultText {
get { return string.Format("The result is: {0:n0}", Operand1 * Operand2); }
}
}
Public Class MultViewModelEx
Public Overridable Property Operand1() As Integer
Public Overridable Property Operand2() As Integer
<DependsOnProperties("Operand1", "Operand2")>
Public ReadOnly Property ResultText() As String
Get
Return String.Format("The result is: {0:n0}", Operand1 * Operand2)
End Get
End Property
End Class
In this approach, you create custom update methods and use a separate Metadata class to link properties with these methods. If the BindableProperty attribute references update methods by name, the OnPropertyChangedCall method uses lambda expressions to retrieve methods. When you rename a custom update method, the Metadata class shows a compilation error.
//View Model code
[System.ComponentModel.DataAnnotations.MetadataType(typeof(Metadata))]
public class SumViewModel_MetaPOCO {
public virtual int Operand1 { get; set; }
public virtual int Operand2 { get; set; }
public virtual int Result { get; set; }
public string ResultText {
get { return string.Format("The result is: {0:n0}", Result); }
}
protected void NotifyResultAndResultTextChanged() {
Result = Operand1 + Operand2;
this.RaisePropertyChanged(x => x.Result);
this.RaisePropertyChanged(x => x.ResultText);
}
//Metadata class
public class Metadata : IMetadataProvider<SumViewModel_MetaPOCO> {
void IMetadataProvider<SumViewModel_MetaPOCO>.BuildMetadata(MetadataBuilder<SumViewModel_MetaPOCO> builder) {
builder.Property(x => x.Result)
.DoNotMakeBindable();
builder.Property(x => x.Operand1).
OnPropertyChangedCall(x => x.NotifyResultAndResultTextChanged());
builder.Property(x => x.Operand2).
OnPropertyChangedCall(x => x.NotifyResultAndResultTextChanged());
}
}
}
Public Class SumViewModel_MetaPOCO
Public Overridable Property Operand1() As Integer
Public Overridable Property Operand2() As Integer
Public Overridable Property Result() As Integer
Public ReadOnly Property ResultText() As String
Get
Return String.Format("The result is: {0:n0}", Result)
End Get
End Property
Protected Sub NotifyResultAndResultTextChanged()
Result = Operand1 + Operand2
Me.RaisePropertyChanged(Function(x) x.Result)
Me.RaisePropertyChanged(Function(x) x.ResultText)
End Sub
'Metadata class
Public Class Metadata
Implements IMetadataProvider(Of SumViewModel_MetaPOCO)
Private Sub IMetadataProviderGeneric_BuildMetadata(ByVal builder As MetadataBuilder(Of SumViewModel_MetaPOCO)) Implements IMetadataProvider(Of SumViewModel_MetaPOCO).BuildMetadata
builder.Property(Function(x) x.Result).DoNotMakeBindable()
builder.Property(Function(x) x.Operand1).OnPropertyChangedCall(Function(x) x.NotifyResultAndResultTextChanged())
builder.Property(Function(x) x.Operand2).OnPropertyChangedCall(Function(x) x.NotifyResultAndResultTextChanged())
End Sub
End Class
End Class
To populate a multi-item control with data source records, use the SetItemsSourceBinding method.
var fluentApi = mvvmContext1.OfType<ViewModelClass>();
fluentApi.SetItemsSourceBinding(
Target
ItemSelector,
SourceSelector,
MatchExpression,
CreateExpression,
DisposeExpression,
ChangeExpression
);
Dim fluentApi = mvvmContext1.OfType(Of ViewModelClass)()
fluentApi.SetItemsSourceBinding(Target ItemSelector, SourceSelector, MatchExpression, CreateExpression, DisposeExpression, ChangeExpression)
In the MVVM Best Practices demo, the following code populates a listbox with objects of a custom Entity class. The SetBinding method binds the editor’s SelectedItem property with the ViewModel SelectedEntity property that retrieves a corresponding Entity object.
//View code
mvvmContext.ViewModelType = typeof(ViewModel);
var fluentApi = mvvmContext.OfType<ViewModel>();
fluentApi.SetItemsSourceBinding(
listBox,
lb => lb.Items,
x => x.Entities,
(item, entity) => object.Equals(item.Value, entity),
entity => new ImageListBoxItem(entity),
null,
(item, entity) => {
((ImageListBoxItem)item).Description = entity.Text;
}
);
fluentApi.SetBinding(listBox, lb => lb.SelectedValue, x => x.SelectedEntity);
//ViewModel code
public class ViewModel {
public virtual Entity SelectedEntity { get; set; }
public virtual ObservableCollection<Entity> Entities { get; set;}
protected void OnSelectedEntityChanged() {
//"Remove" is a custom ViewModel method that deletes a selected entity
this.RaiseCanExecuteChanged(x => x.Remove());
}
protected void OnEntitiesChanged() {
SelectedEntity = Entities.FirstOrDefault();
}
}
//Model code
public class Entity {
public Entity(int id) {
this.ID = id;
this.Text = "Entity " + id.ToString();
}
public int ID { get; private set; }
public string Text { get; set; }
}
'View code
mvvmContext.ViewModelType = GetType(ViewModel)
Dim fluentApi = mvvmContext.OfType(Of ViewModel)()
fluentApi.SetItemsSourceBinding(
listBox,
Function(lb) lb.Items,
Function(x) x.Entities,
Function(item, entity) Object.Equals(item.Value, entity),
Function(entity) New ImageListBoxItem(entity),
Nothing,
Function(item, entity) CType(item, ImageListBoxItem).Description = entity.Text
)
fluentApi.SetBinding(listBox, Function(lb) lb.SelectedValue, Function(x) x.SelectedEntity)
'ViewModel code
Public Class ViewModel
Public Overridable Property SelectedEntity() As Entity
Public Overridable Property Entities() As ObservableCollection(Of Entity)
Protected Sub OnSelectedEntityChanged()
'"Remove" is a custom ViewModel method that deletes a selected entity
Me.RaiseCanExecuteChanged(Function(x) x.Remove())
End Sub
Protected Sub OnEntitiesChanged()
SelectedEntity = Entities.FirstOrDefault()
End Sub
End Class
'Model code
Public Class Entity
Public Sub New(ByVal id As Integer)
Me.ID = id
Me.Text = "Entity " & id.ToString()
End Sub
Private privateID As Integer
Public Property ID() As Integer
Get
Return privateID
End Get
Private Set(ByVal value As Integer)
privateID = value
End Set
End Property
Public Property Text() As String
End Class
Triggers allow you to modify a UI (View) when a ViewModel property changes. In the DevExpress demo, a checkbox is bound to the ViewModel “IsActive” property. When this property’s value changes, the Trigger changes the background color of a UI element (label).
//ViewModel code
public class ViewModel {
public virtual bool IsActive { get; set; }
}
//ViewModel code
var fluent = mvvmContext.OfType<ViewModel>();
fluent.SetBinding(checkEdit, c => c.Checked, x => x.IsActive);
fluent.SetTrigger(x => x.IsActive, (active) => {
if(active)
label.Appearance.BackColor = Color.LightPink;
else
label.Appearance.BackColor = Color.Empty;
});
'ViewModel code
Public Class ViewModel
Public Overridable Property IsActive() As Boolean
End Class
'ViewModel code
Private fluent = mvvmContext.OfType(Of ViewModel)()
fluent.SetBinding(checkEdit, Function(c) c.Checked, Function(x) x.IsActive)
fluent.SetTrigger(Function(x) x.IsActive, Sub(active)
If active Then
label.Appearance.BackColor = Color.LightPink
Else
label.Appearance.BackColor = Color.Empty
End If
End Sub)