Data and Property Bindings
- 17 minutes to read
Depending on what properties you bind, there are three possible scenarios:
- Regular Binding - a ViewModel property is bound to a property of any non-editable View element. Since the element is non-editable, you do not need to send update notifications back to the bound property (one-way binding).
- Data Binding - a Model property (data field) is bound to an editor property. If users can change editor values, you need to update bound properties (two-way binding).
- Property Dependencies - two properties from the same ViewModel are bound.
Regular Binding
If you need to pass data from a property to a property from another ViewModel, you can use the standard DataBindings API or we recommend the DevExpress MvvmContext.SetBinding
method.
For example, a View has a LabelControl with no text. A ViewModel has a bindable string “LabelText” property. Use any of the following approaches to pass the property value to this Label.
//ViewModel code
[POCOViewModel()]
public class Form1ViewModel {
public Form1ViewModel() {
LabelText = "Value stored in ViewModel";
}
public virtual string LabelText { get; set; }
}
//View code
//option #1 (recommended): SetBinding method
var fluent = mvvmContext1.OfType<Form1ViewModel>();
fluent.SetBinding(labelControl1, l => l.Text, x=>x.LabelText);
//option #2: DataBindings
Form1ViewModel viewModel = mvvmContext1.GetViewModel<Form1ViewModel>();
labelControl1.DataBindings.Add("Text", viewModel, "LabelText");
Tip
If you need to bind an editor to a property, and choose a specific update mode, use standard data bindings instead of the SetBinding
method (see the Data Binding section).
Update Notifications in POCO ViewModels
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.
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.
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
.
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; }
}
If you want a specific method to be called each time such property is updated, specify this method’s name in the same attribute.
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;
}
}
}
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.
Bind Properties of Nested and Non-POCO ViewModels
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);
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);
Data Binding
To bind an editor to a Model property, add a 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.
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; }
}
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);
…but in this case you lose the option to set a required DataSourceUpdateMode
, which allows you to prevent excessive update notifications.
Property Dependencies
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:
- Two TextEdit controls are bound to ViewModel “Operand1” and “Operand 2” properties.
- When a user changes TextEdit values, Operand properties refresh their values.
- When Operand properties change, they update the numeric “Result” property (dependency #1).
- The “Result” property updates the string “ResultText” property (dependency #2).
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);
However, property dependencies are declared differently in every module.
OnPropertyChanged Methods
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);
}
}
Custom Update Methods
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 code sample below, 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);
}
}
DependsOn Attribute
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); }
}
}
Metadata Classes
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());
}
}
}
Collection Bindings
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
);
- Target - a target UI element that you need to populate.
- Item Selector - an expression that retrieves the UI element’s item collection that should be populated from a data source.
- Source Selector - an expression that locates a data source whose items should be used to populate the Target.
- Match Expression - an expression that compares a data source item with a Target child item. When you change or remove a data source record, the Framework runs this expression to determine whether it should update a corresponding Target collection item.
- Create Expression - an expression that creates a new Target collection item when a new data source record appears.
- Dispose Expression - an expression that disposes of a Target collection item when its related data source record is removed.
- Change Expression - specifies how to update a Target collection item when the Match Expression concludes this item differs from a data source record.
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; }
}
Triggers
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;
});