Saturday, May 30, 2015

Windows Form MVVM databinding

I inherited a complex Windows Form UserControl that contains a DataGridView showing a matrix data structure like this.

The code behind takes care of the generation of the headers and cell values. It also takes care of the cell color scheme based on the value of the cell.

Because of the complexity and the fact that there's no out-of-the-box DataGridView implementation for WPF application, I decided to reuse this Windows Form UserControl in a WPF application I was working on. The problem is that Windows Form does not support data binding in WPF MVVM context. We will need to do the plumbing ourselves to be able to use the data binding functionality. One way to do this is as follows:

  1. Wrap the Windows Form UserControl in a WPF UserControl by using WindowsFormsHost
  2. Create DependencyProperty in this WPF UserControl, which we can use to bind to a viewmodel
  3. Use PropertyChangedCallback to modify property or state in the DataGridView control
  4. Use events in the DataGridView control to return property or state back to the DependencyProperty in the WPF UserControl

I'm posting a simple version of the original source code to illustrate this. Let's start with the business objects.

The Business Objects

    public class TenorStrikeRate
    {
        public TenorStrikeRate(string tenor, double strike, double rate)
        {
            Tenor = tenor;
            Strike = strike;
            Rate = rate;
        }

        public string Tenor { get; set; }

        public double Strike { get; set; }

        public double Rate { get; set; }
    }
    public class TenorStrikeRates : IEnumerable<TenorStrikeRate>
    {
        private readonly SortedList<Tuple<string, double>, TenorStrikeRate> internalData;

        public TenorStrikeRates()
        {
            internalData = new SortedList<Tuple<string, double>, TenorStrikeRate>();
        }

        public List<string> UniqueSortedTenors
        {
            get { return internalData.Values.Select(x => x.Tenor).Distinct().ToList(); }
        }

        public List<double> UniqueSortedStrikes
        {
            get { return internalData.Values.Select(x => x.Strike).Distinct().ToList(); }
        }

        public void Add(TenorStrikeRate toBeAddedItem)
        {
            Tuple<string, double> key = new Tuple<string, double>(toBeAddedItem.Tenor, toBeAddedItem.Strike);
            if (internalData.ContainsKey(key))
            {
                internalData.Remove(key);
            }

            internalData.Add(key, toBeAddedItem);
        }

        public TenorStrikeRate Find(string tenor, double strike)
        {
            return internalData.Values.FirstOrDefault(x => IsTenorEqual(x.Tenor, tenor) && IsDoubleEqual(x.Strike, strike));
        }

        public IEnumerator<TenorStrikeRate> GetEnumerator()
        {
            IEnumerator<TenorStrikeRate> iterator = internalData.Values.GetEnumerator();
            while (iterator.MoveNext())
            {
                yield return iterator.Current;
            }
        }

        IEnumerator IEnumerable.GetEnumerator()
        {
            return GetEnumerator();
        }

        public override bool Equals(object obj)
        {
            if (ReferenceEquals(null, obj))
                return false;
            if (ReferenceEquals(this, obj))
                return true;
            if (obj.GetType() != typeof(TenorStrikeRates))
                return false;

            TenorStrikeRates other = (TenorStrikeRates)obj;
            return IsAllItemInListEqual(internalData.Values, other.internalData.Values);
        }
        
        private bool IsAllItemInListEqual(IList<TenorStrikeRate> thisList, IList<TenorStrikeRate> otherList)
        {
            bool isEqual = thisList.Count.Equals(otherList.Count);
            for (int index = 0; index < otherList.Count && isEqual; index++)
            {
                TenorStrikeRate thisItem = thisList[index];
                TenorStrikeRate otherItem = otherList[index];
                isEqual = IsTenorEqual(thisItem.Tenor, otherItem.Tenor) 
                    && IsDoubleEqual(thisItem.Strike, otherItem.Strike)
                    && IsDoubleEqual(thisItem.Rate, otherItem.Rate);
            }

            return isEqual;
        }

        private bool IsDoubleEqual(double one, double two)
        {
            if (double.IsNaN(one) && double.IsNaN(two))
            {
                return true;
            }

            return Math.Abs(one - two) < 1e-15;
        }

        private bool IsTenorEqual(string one, string two)
        {
            return one.Equals(two, StringComparison.InvariantCultureIgnoreCase);
        }
    }

The Windows Form UserControl

The Windows Form UserControl contains a DataGridView named "dgv". Set(TenorStrikeRates source) populates the DataGridView. GetDataGridSource() returns the current source state of the DataGridView. In this class we also need to declare a public event to return the current data grid source to the subscribers. A situation where we want to return the current data grid source is when users perform a data grid cell editing operation. We can then hook up this public event to the DataGridView CellEndEdit event which occurs when edit mode stops for the currently selected cell.

    public partial class DataGridViewUc : System.Windows.Forms.UserControl
    {
        public DataGridViewUc()
        {
            InitializeComponent();
        }

        public delegate void ReturnGridSourceEventHandler(TenorStrikeRates currentGridSource);

        public event ReturnGridSourceEventHandler ReturnDataGridSource;

        public void Set(TenorStrikeRates source)
        {
            dgv.Columns.Clear();
            dgv.Rows.Clear();
            if (source != null)
            {
                PopulateColumnHeader(source.UniqueSortedStrikes);
                PopulateRowHeader(source.UniqueSortedTenors);
                PopulateCells(source);
            }
        }

        public TenorStrikeRates GetDataGridSource()
        {
            TenorStrikeRates quotes = new TenorStrikeRates();
            foreach (DataGridViewColumn column in dgv.Columns)
            {
                string columnHeaderValue = column.HeaderText;
                if (columnHeaderValue != null)
                {
                    double strike = ReadStrike(columnHeaderValue);
                    foreach (DataGridViewRow row in dgv.Rows)
                    {
                        object rowHeaderValue = row.HeaderCell.Value;
                        if (rowHeaderValue != null)
                        {
                            string tenor = rowHeaderValue.ToString();
                            object rateValue = row.Cells[column.Name].Value;
                            double rate = rateValue == null ? double.NaN : Convert.ToDouble(rateValue);
                            quotes.Add(new TenorStrikeRate(tenor, strike, rate));
                        }
                    }
                }
            }

            return quotes;
        }

        private void PopulateColumnHeader(List<double> strikes)
        {
            foreach (double strike in strikes)
            {
                string headerText = strike.ToString("P2");
                dgv.Columns.Add(headerText, headerText);
            }
        }

        private void PopulateRowHeader(List<string> tenors)
        {
            int numberOfRows = tenors.Count;
            dgv.Rows.Add(numberOfRows);
            for (int i = 0; i < numberOfRows; i++)
            {
                dgv.Rows[i].HeaderCell.Value = tenors[i];
            }
        }

        private void PopulateCells(TenorStrikeRates quotes)
        {
            List<string> tenors = quotes.UniqueSortedTenors;
            List<double> strikes = quotes.UniqueSortedStrikes;
            for (int rowIdx = 0; rowIdx < tenors.Count; rowIdx++)
            {
                string optionTenor = tenors[rowIdx];
                for (int colIdx = 0; colIdx < strikes.Count; colIdx++)
                {
                    double strike = strikes[colIdx];
                    TenorStrikeRate quote = quotes.Find(optionTenor, strike);
                    if (quote != null)
                    {
                        double rate = quote.Rate;
                        DataGridViewCell cell = dgv.Rows[rowIdx].Cells[colIdx];
                        cell.Value = rate;
                    }
                }
            }
        }
        
        private double ReadStrike(string input)
        {
            string percentSymbol = Thread.CurrentThread.CurrentCulture.NumberFormat.PercentSymbol;
            input = input.Replace(percentSymbol, string.Empty);
            return double.Parse(input, Thread.CurrentThread.CurrentCulture.NumberFormat) / 100;
        }

        private void dgv_CellEndEdit(object sender, DataGridViewCellEventArgs e)
        {
            if (ReturnDataGridSource != null)
            {
                TenorStrikeRates newSource = GetDataGridSource(); 
                ReturnDataGridSource(newSource);
            }
        }
    }

The WPF Host

To be able to bind the DataGridView UserControl to a viewmodel, we will need to wrap it in a WPF UserControl by using WindowsFormsHost. Then we define DependencyProperty GridSource that we can bind to a property in the viewmodel. Next we define PropertyChangedCallback OnGridSourcePropertyChanged, which is called whenever GridSource property changed. In the callback, we pass in the new source to the DataGridView so it can update its display. The WPF UserControl needs to subscribe to the DataGridView's ReturnDataGridSource event to make sure any changes in the DataGridView is propagated to the WPF host layer.

XAML:


<UserControl x:Class="DataGridViewHostedInWpfControl.WpfHost.DataGridViewUcHost"
             xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
             xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
             xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" 
             xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
             xmlns:local="clr-namespace:DataGridViewHostedInWpfControl.WinFormLayer"
             mc:Ignorable="d" 
             d:DesignHeight="300" d:DesignWidth="300">
    
    <WindowsFormsHost x:Name="MyWinFormsHost">
        <local:DataGridViewUc />
    </WindowsFormsHost>

</UserControl>

Code behind:

    public partial class DataGridViewUcHost : System.Windows.Controls.UserControl
    {
        public static readonly DependencyProperty GridSourceProperty = DependencyProperty.Register(
             "GridSource",
             typeof(TenorStrikeRates),
             typeof(DataGridViewUcHost),
             new FrameworkPropertyMetadata(null, FrameworkPropertyMetadataOptions.BindsTwoWayByDefault, OnGridSourcePropertyChanged));

        public TenorStrikeRates GridSource
        {
            get { return (TenorStrikeRates)GetValue(GridSourceProperty); }
            set { SetValue(GridSourceProperty, value); }
        }

        private static DataGridViewUc dgvUc;

        public DataGridViewUcHost()
        {
            InitializeComponent();

            dgvUc = (DataGridViewUc)MyWinFormsHost.Child;
            dgvUc.ReturnDataGridSource += OnReturnDataGridSource;
        }

        private static void OnGridSourcePropertyChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
        {
            TenorStrikeRates newGridSource = (TenorStrikeRates)e.NewValue;
            TenorStrikeRates currentGridSource = dgvUc.GetDataGridSource();
            if (!currentGridSource.Equals(newGridSource))
            {
                dgvUc.Set(newGridSource);
            }
        }

        private void OnReturnDataGridSource(TenorStrikeRates currentGridSource)
        {
            GridSource = currentGridSource;
        }
    }

The MainWindow

Now let's build a small demo. Create a StackPanel containing the WPF control that hosts our DataGridView UserControl. Add a TextBox to the StackPanel. Everytime we change a value in the data grid, the change will be shown in the TextBox.

XAML:


<Window x:Class="DataGridViewHostedInWpfControl.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:local="clr-namespace:DataGridViewHostedInWpfControl.WpfHost"
        Title="MainWindow" Height="350" Width="525">
    
    <StackPanel>
        <Label Content="Input" />
        <local:DataGridViewUcHost GridSource="{Binding Input}" MinWidth="300" MinHeight="100" Margin="10" />
        <Label Content="Output" />
        <TextBox Text="{Binding Output}" />
    </StackPanel>

</Window>

Code behind:

    public partial class MainWindow : System.Windows.Window
    {
        public MainWindow()
        {
            MainViewModel vm = new MainViewModel();
            DataContext = vm;

            InitializeComponent();

            vm.Input = BuildInitialInput();
        }

        private TenorStrikeRates BuildInitialInput()
        {
            var quotes = new TenorStrikeRates();
            quotes.Add(new TenorStrikeRate("1y", -0.01, 0.01));
            quotes.Add(new TenorStrikeRate("1y", 0, 0.02));
            quotes.Add(new TenorStrikeRate("1y", 0.01, 0.03));
            quotes.Add(new TenorStrikeRate("2y", -0.01, 0.04));
            quotes.Add(new TenorStrikeRate("2y", 0, 0.05));
            quotes.Add(new TenorStrikeRate("2y", 0.01, 0.06));
            return quotes;
        }
    }

ViewModel:

    public class MainViewModel : INotifyPropertyChanged
    {
        private TenorStrikeRates input;
        public TenorStrikeRates Input
        {
            get { return input; }
            set
            {
                input = value;
                OnPropertyChanged("Input");
                Output = BuildOutputValue(value);
            }
        }

        private string output;
        public string Output
        {
            get { return output; }
            set
            {
                output = value;
                OnPropertyChanged("Output");
            }
        }

        public event PropertyChangedEventHandler PropertyChanged;

        protected virtual void OnPropertyChanged(string propertyName)
        {
            PropertyChangedEventHandler handler = PropertyChanged;
            if (handler != null)
            {
                handler(this, new PropertyChangedEventArgs(propertyName));
            }
        }

        private string BuildOutputValue(TenorStrikeRates tenorStrikeRates)
        {
            string text = string.Empty;
            foreach (TenorStrikeRate item in tenorStrikeRates)
            {
                text = text + string.Format("Tenor: {0}, Strike: {1}, Rate: {2}\n", item.Tenor, item.Strike.ToString("P2"), item.Rate);
            }

            return text;
        }
    }

Source Code

https://github.com/velianarie/DataGridViewHostedInWpfControl