Home > Articles > Better way to use binding in the WPF applications

Articles

Better way to use binding in the WPF applications

Common goal of the binding is notifying business objects by the UI elements about changes of the UI elements' values and setting new values to the appropriate properties of the bound business object.

We will review another way of the binding in this article - our business object will notify UI element about changes itself and UI element will be redrawn according to changes in the business objects.

What are benefits of this approach? Well, let's consider the situation: we have business objects with sophisticated logic - for example, some graphic objects that can be moved by mouse, but they can't be put over other business objects. Of course we can specify logic of UI elements in the UI layer, but it would be better to separate logic into business layer. This WPF technique we have used in building the architecture in the project of automation of the railroad operations, which helped to achieve following results:

  1. Flexible changes of the user interface - appearance of the UI element can be changed easily in the XAML file and we do not need to change the code-behind file in case if business object has not changed itself.
  2. No code of interaction between UI elements and business objects is needed - because WPF architecture changing the business object forces UI element to redraw itself.
  3. Avoid mixing business logic with UI logic.

As example for article let's use simple application, which is able to draw lines and move them with the mouse, so the simple line gets the role of business object. WPF UI element will be able to track business object state, if the class of this object implements INotifyPropertyChanged interface. This interface is very simple and requires thу class to have PropertyChanged event with type PropertyChangedEventHandler. We just need to call this event when any property is changed to notify subscribers about changes in the object. Below is a code of the class, we called SimpleLine:

using System.ComponentModel;
using System.Windows;
using System.Windows.Media;

namespace WpfArticleApplication
{
  public class SimpleLine : INotifyPropertyChanged
  {
    #region INotifyPropertyChanged Members

    public event PropertyChangedEventHandler PropertyChanged;

    #endregion

    /// <summary>
    /// Helper method for calling PropertyChanged event
    /// </summary>
    /// <param name="propertyName"></param>
    protected void OnPropertyChanged(string propertyName)
    {
      if (PropertyChanged != null)
        PropertyChanged(this, new PropertyChangedEventArgs(propertyName));
    }

    /// <summary>
    /// Start point of line
    /// </summary>
    private Point _p1;

    /// <summary>
    /// X position data of start point
    /// </summary>
    public double X1
    {
      get { return _p1.X; }
      set
      {
        _p1.X = value;
        OnPropertyChanged("X1");
      }
    }

    /// <summary>
    /// Y position data of start point
    /// </summary>
    public double Y1
    {
      get { return _p1.Y; }
      set
      {
        _p1.Y = value;
        OnPropertyChanged("Y1");
      }
    }

    /// <summary>
    /// End point of line
    /// </summary>
    private Point _p2;

    /// <summary>
    /// X position data of end point
    /// </summary>
    public double X2
    {
      get { return _p2.X; }
      set
      {
        _p2.X = value;
        OnPropertyChanged("X2");
      }
    }

    /// <summary>
    /// Y position data of end point
    /// </summary>
    public double Y2
    {
      get { return _p2.Y; }
      set
      {
        _p2.Y = value;
        OnPropertyChanged("Y2");
      }
    }

    /// <summary>
    /// Color line
    /// In our case only black when line is not selected,
    /// or red when line is selected
    /// </summary>
    public SolidColorBrush Color
    {
      get { return _isSelected ? Brushes.Red : Brushes.Black; }
    }

    /// <summary>
    /// Line is selected
    /// </summary>
    private bool _isSelected = false;
    public bool IsSelected
    {
      get { return _isSelected; }
      set
      {
        _isSelected = value;
        OnPropertyChanged("Color");
        OnPropertyChanged("IsSelected");
      }
    }
    
    /// <summary>
    /// Constructor of our simple line
    /// </summary>
    /// <param name="p1">The start point of line</param>
    /// <param name="p2">The end point of line</param>
    public SimpleLine(Point p1, Point p2)
    {
      _p1 = p1;
      _p2 = p2;
    }

  }
}

It could be inferred from the above code that access method set for almost all properties calls our helper method OnPropertyChanged, which calls the PropertyChanged event and passes to it parameter of type PropertyChangedEventArgs, containing the name of changed property. Please note, that method OnPropertyChanged was added only to keep off duplicating code.
Of course, drawing only one line is not very interesting, so there is need to have ability to add or remove lines - for this purpose we will use collection of type ObservableCollection.This collection will notify WPF UI elements about it's changes. As discussed above we want to separate business logic of business object from the UI layer, having implemented for this goal editor class, called SimpleLineEditor:

using System;
using System.Collections.ObjectModel;
using System.Windows;

namespace WpfArticleApplication
{
  public class SimpleLineEditor
  {
    /// <summary>
    /// Collection of our lines
    /// </summary>
    private ObservableCollection<SimpleLine> _data;
    public ObservableCollection<SimpleLine> Data
    {
      get { return _data; }
      set { _data = value; }
    }

    /// <summary>
    /// SimpleLine object which is now acted
    /// </summary>
    private SimpleLine _activeSimpleLine = null;

    /// <summary>
    /// If there is active simple line, then our editor is active
    /// </summary>
    public bool IsActive
    {
      get { return _activeSimpleLine != null; }
    }

    /// <summary>
    /// Random generator, which helps us to create lines
    /// </summary>
    private Random _rndGenerator = new Random();

    public SimpleLineEditor()
    {
      // Create a collection of our business objects
      _data = new ObservableCollection<SimpleLine>();
      // Fill collection with default lines
      _data.Add(new SimpleLine(new Point(30, 10), new Point(30, 50)));
      _data.Add(new SimpleLine(new Point(10, 30), new Point(50, 30)));
    }

    /// <summary>
    /// Add random line to the collection of our lines
    /// </summary>
    public void AddRandomLine()
    {
      Point p1 = new Point(_rndGenerator.NextDouble() * 770, _rndGenerator.NextDouble() * 330);
      Point p2 = new Point(_rndGenerator.NextDouble() * 770, _rndGenerator.NextDouble() * 330);
      SimpleLine sl = new SimpleLine(p1, p2);
      _data.Add(sl);    
    }

    /// <summary>
    /// Set active simple line
    /// </summary>
    public void SetActiveLine(SimpleLine activeSimpleLine)
    {
      _activeSimpleLine = activeSimpleLine;
      _activeSimpleLine.IsSelected = true;
    }

    /// <summary>
    /// Unset active simple line
    /// </summary>
    public void UnsetActiveLine()
    {
      if (_activeSimpleLine != null)
        _activeSimpleLine.IsSelected = false;
      
      _activeSimpleLine = null;
    }

    /// <summary>
    /// Change active line position
    /// </summary>
    /// <param name="deltaX"></param>
    /// <param name="deltaY"></param>
    internal void SetActiveLinePosition(double deltaX, double deltaY)
    {
      _activeSimpleLine.X1 = _activeSimpleLine.X1 - deltaX;
      _activeSimpleLine.Y1 = _activeSimpleLine.Y1 - deltaY;
      _activeSimpleLine.X2 = _activeSimpleLine.X2 - deltaX;
      _activeSimpleLine.Y2 = _activeSimpleLine.Y2 - deltaY;
    }
  }
}

Editor class contains collection of the SimpleLine objects and allows adding random lines, setting/unsetting active line, setting active line position by delta. Now all is ready to implement WPF application, let's see the XAML code of our window:

<Window x:Class="WpfArticleApplication.MainWindow"
  xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
  xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
  Title="MainWindow" Height="400" Width="800">
  <StackPanel>
    <Button Height="23" Name="addLineButton" Width="Auto" Click="addLineButton_Click">Add Line!</Button>
    <Canvas Name="mainCanvas" Width="770" Height="330" Background="White"
        MouseDown="mainCanvas_MouseDown"
        MouseUp="mainCanvas_MouseUp"
        MouseMove="mainCanvas_MouseMove">
      <ItemsControl Name="linesItemsControl" Width="770" Height="330">
        <ItemsControl.ItemsPanel>
          <ItemsPanelTemplate>
            <Canvas/>
          </ItemsPanelTemplate>
        </ItemsControl.ItemsPanel>
        <ItemsControl.ItemTemplate>
          <DataTemplate>
            <Line Canvas.Top="0" Canvas.Left="0"
              X1="{Binding Path=X1}"
              Y1="{Binding Path=Y1}"
              X2="{Binding Path=X2}"
              Y2="{Binding Path=Y2}"
              Stroke="{Binding Path=Color}"
              StrokeThickness="5"
              StrokeEndLineCap="Round"
              StrokeStartLineCap="Round"></Line>
          </DataTemplate>
        </ItemsControl.ItemTemplate>
      </ItemsControl>
    </Canvas>
  </StackPanel>
</Window>

This window contains following elements:

  1. addLineButton - Button element, which will add random line, calling the addLineButton_Click method.
  2. mainCanvas - Canvas element, which displays lines and handles mouse up/down/move events.
  3. linesItemsControl - ItemsControl element which draws lines on the canvas using DataTemplate. As you see DataTemplate contains only Line element, with binding appropriate properties to the data source element SimpleLine.

Let’s have a look at the code-behind file:

using System;
using System.Collections.ObjectModel;
using System.Windows;
using System.Windows.Input;

namespace WpfArticleApplication
{
  public partial class MainWindow : Window
  {
    /// <summary>
    /// Business objects edit logic
    /// </summary>
    SimpleLineEditor _simpleLineEditor;

    /// <summary>
    /// Point, to remember last position of mouse
    /// </summary>
    private Point _initialPoint = new Point();

    /// <summary>
    /// Window constructor
    /// </summary>
    public MainWindow()
    {
      InitializeComponent();

      _simpleLineEditor = new SimpleLineEditor();

      // Set items source as a collection of our simple lines
      linesItemsControl.ItemsSource = _simpleLineEditor.Data;
    }

    /// <summary>
    /// Add random line
    /// </summary>
    /// <param name="sender"></param>
    /// <param name="e"></param>
    private void addLineButton_Click(object sender, RoutedEventArgs e)
    {
      _simpleLineEditor.AddRandomLine();
    }

    /// <summary>
    /// Event handler of mouse down
    /// </summary>
    /// <param name="sender"></param>
    /// <param name="e"></param>
    private void mainCanvas_MouseDown(object sender, MouseButtonEventArgs e)
    {
      /// we get business object SimpleLine which is called this event
      if (e.OriginalSource is FrameworkElement
        && ((FrameworkElement)e.OriginalSource).DataContext is SimpleLine)
      {
        SimpleLine activeSimpleLine = (SimpleLine)((FrameworkElement)e.OriginalSource).DataContext;
        _simpleLineEditor.SetActiveLine(activeSimpleLine);

        // get initial point where mouse is down
        _initialPoint = Mouse.GetPosition(mainCanvas);
        Mouse.Capture(mainCanvas);
      }
    }

    /// <summary>
    /// Event handler of mouse move
    /// </summary>
    /// <param name="sender"></param>
    /// <param name="e"></param>
    private void mainCanvas_MouseMove(object sender, MouseEventArgs e)
    {
      if (_simpleLineEditor.IsActive)
      {
        // Calculate delta of mouse
        Point currentPoint = Mouse.GetPosition(mainCanvas);
        double deltaX = (_initialPoint.X - currentPoint.X);
        double deltaY = (_initialPoint.Y - currentPoint.Y);

        // Tell that mouse position is cchanged to our editor
        _simpleLineEditor.SetActiveLinePosition(deltaX, deltaY);

        // Remeber last position of mouse
        _initialPoint = currentPoint;
      }
    }

    /// <summary>
    /// Event handler of mouse up
    /// </summary>
    /// <param name="sender"></param>
    /// <param name="e"></param>
    private void mainCanvas_MouseUp(object sender, MouseButtonEventArgs e)
    {
      if (_simpleLineEditor.IsActive)
      {
        Mouse.Capture(null);
        // unset active line
        _simpleLineEditor.UnsetActiveLine();
      }
    }
  }
}

In the constructor of the window we create SimpleLineEditor object, which is responsible for SimpleLine edit. Additionally we set items source for linesItemsControl. Items source is a collection of type ObservableCollection, which is defined and filled with sample data in the SimpleLineEditor object. You can see that we do not change UI elements directly, we just call SimpleLineEditor methods for changing business objects. Editor class implements business logic inside and it can be easily changed without changes in the UI layer. So now our application is ready and you can download source code here to try it.

Nikita Akatev, Digital Design