Data and Property Bindings

  • 16 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.

simple-b

//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");

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.

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.

createVM

You can also use the ViewModelType property to do this in code.

mvvmContext.ViewModelType = typeof(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; }
NOTE

The Framework ignores properties with a backing field. Label such properties with the DevExpress.Mvvm.DataAnnotations.BindableProperty attribute to be able to bind such properties.

using DevExpress.Mvvm.DataAnnotations;
//. . .
string name;
[BindableProperty]
public virtual string Name {
    get { return name; }
    set { name = value; }
}

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.

oneway-twoway

Run the demo

//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.

Run the demo

//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.

Run the demo

//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 immediatelly or when the editor is validated.

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; }
}

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).

dependencies

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.

Run the Demo

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.

Run the Demo

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;
});