OpenLightGroup Blog

rss

Blogs from OpenLightGroup.net


PollResultsBar – Binding to Multiple Values Through Inheritance and Custom Dependency Properties

I recently had the requirement to create an interface similar to the following:

image

The item of interest is the colored bar that shows the ratio between agreements and disagreements. We can use a ProgressBar and set the Maximum property  to the total of the Agree and Disagree counts, and set the Value property to the Agree count. Nothing very tricky except how to bind the Maximum property in the ProgressBar to two separate properties in the ViewModel.

This may not be the best solution, but it’s a good example of how you can use inheritance and custom Dependency Properties to solve the problem.

DownloadCode:

http://richardwaddell.adefwebserver.com/PollResultsBarDemo.zip

To start we’ll create a Silverlight application with hosting web site.  Here’s a step-by-step if you need it; you can ignore all the stuff about isolating the code in a separate Silverlight class library for this simple example, just do the steps to create the Silverlight project and select the option to create the hosting website.

First we’ll create a PollResults class:

namespace PollResultsBarDemo
{
    public class PollResults
    {
        public PollResults(string statement, int agreeCount, int disagreeCount)
        {
            Statement = statement;
            AgreeCount = agreeCount;
            DisagreeCount = disagreeCount;
        }
        public string Statement { get; set; }
        public int AgreeCount { get; set; }
        public int DisagreeCount { get; set; }
    }
}

Then a PollResultsBarDemoModel class to provide a GetPollResults method:

namespace PollResultsBarDemo
{
    public class PollResultsBarDemoModel
    {
        public static ObservableCollection<PollResults> GetPollResults()
        {
            ObservableCollection<PollResults> colResults = new ObservableCollection<PollResults>();
            colResults.Add(new PollResults("I'm coo-coo for Cocoa Pops", 170, 45));
            colResults.Add(new PollResults("Dogs are better than cats", 60, 60));
            colResults.Add(new PollResults("The world will end in 2012", 15, 30));
            return colResults;
        }
    }
}

And a MainPageViewModel class to consume it. You’ll notice I called GetPollResults() from the constructor. In the real-world you may or may not want to do this, and you would probably provide a GetPollResultsCommand ICommand property that can be invoked from the View.

Speaking of the View, open MainPage.xaml in Expression Blend:

image

At this point I ran into a problem in that Blend initially couldn’t see MainPageViewModel even after re-building and opening and closing. Eventually I removed the constructor and re-built at which point Blend could see it. I put the constructor back in and fortunately Blend could still see it. So I was able to move ahead with:

We want to assign MainPageViewModel to the DataContext property of UserControl.

  1. Select UserControl in Objects and Timeline
  2. Locate the DataContext property
  3. Click the New button next to it
  4. A Select Object dialog will appear
  5. Select MainPageViewModel

image

Once you click on ‘OK’, you can select the Data tab and see that PollResultsList is available:

image

(I did this a little out of order, so the ListBox I’m about to add already appears in the screen shots above.)

Add a ListBox to LayoutRoot. The easiest way is to, with LayoutRoot selected, click on the little white triangle on the icon above the Assets icon, whose default image will be a Button icon, as shown.  Then click on ListBox on the dropdown and the ListBox icon will replace the Button icon on the Tools Panel. Double-click on the ListBox icon and one will be inserted into LayoutRoot.

image

Drag PollResultsList onto UserControl so that you see the message shown below:

image

I’m a big fan of instant gratification, so I always run stuff the first time I think something will show up:

image

Setting Width and Height to Auto and the alignment to Stretch will cause the ListBox to fill LayoutRoot:

image

The problem with that is at run-time you get a ListBox that fills the entire browser window.  For this example I’m going to continue to fill LayoutRoot, which fills UserControl, with the ListBox, so let’s constrain UserControl to 400 x 600.

image

In case you don’t know, the little white squares next to Width and Height (Advanced Option icons) indicate that they are set to other than their default values. If they were still at their default values the Advanced Option icons would appear gray, as you see next to HorizontalAlignment in the line below Height.

Don’t confuse default with the initial settings you get when you add a new control in Blend. Quite often they arrive with the Advanced Options icon set to white.

We need to reformat the list box entries which means editing the ListBox ItemTemplate. Right-click on ListBox, then select Edit Additional Templates/ Edit Generated Items (ItemTemplate) / Edit Current:

image

As you can see, our current ItemTemplate is a vertically oriented StackPanel containing three TextBoxes.

image

We can sort this out pretty quick. Move the bottom TextBlock above the other two and set its Width to Auto:

image

Add a new StackPanel to the existing one and set its Width and Height to Auto and  Orientation to Horizontal:

image

Starting with the second of the three TextBlocks, cut and paste the bottom two into the new StackPanel.

image

Now all we need is a PollResultsBar to place between the two TextBlocks in the second StackPanel:

using System;
using System.Net;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Documents;
using System.Windows.Ink;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Media.Animation;
using System.Windows.Shapes;
using System.ComponentModel;
namespace PollResultsBarDemo
{
    public class PollResultsBar : ProgressBar
    {
        public static readonly DependencyProperty LeftValueProperty =
             DependencyProperty.Register("LeftValue",
                                             typeof(int),
                                             typeof(PollResultsBar),
                                             new PropertyMetadata(0, new PropertyChangedCallback(OnLeftValueChanged)));
        public static readonly DependencyProperty RightValueProperty =
             DependencyProperty.Register("RightValue",
                                             typeof(int),
                                             typeof(PollResultsBar),
                                             new PropertyMetadata(0, new PropertyChangedCallback(OnRightValueChanged)));
        public int LeftValue
        {
            get { return (int)base.GetValue(LeftValueProperty); }
            set { base.SetValue(LeftValueProperty, value); }
        }
        public int RightValue
        {
            get { return (int)base.GetValue(RightValueProperty); }
            set { base.SetValue(RightValueProperty, value); }
        }
        public static void OnLeftValueChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
        {
            SetValue(d as PollResultsBar, (int)e.NewValue);
            SetMaximum(d as PollResultsBar);
        }
        public static void SetValue(PollResultsBar bar, int value)
        {
            bar.Value = value;
        }
        public static void OnRightValueChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
        {
            SetMaximum(d as PollResultsBar);
        }
        private static void SetMaximum(PollResultsBar bar)
        {
            bar.Maximum = bar.LeftValue + bar.RightValue;
        }
    }
}
  • The Dependency Properties allow Blend to enable binding to PollResultsBar LeftValue and RightValue.
  • Each Dependency property Registration specifies a method to be called when the property value changes.
  • These methods, OnLeftValueChanged and OnRightValueChanged cast the DependencyObject argument ‘d’ to PollResultsBar and calls SetMaximum()
  • SetMaximum() sets the ProgressBar Maximum property to the sum of LeftValue and RightValue.
  • OnLeftValueChanged calls SetValue() which sets the ProgressBar.Value property to (int)e.NewValue.

After re-building the project, I had to fiddle with the code and closing and re-opening Blend to get the PollResultsBar control is available in Assets / Project:

image

Add it to StackPanel and move it between the two TextBlocks. I also added a margin to the left of PollResultsBar and the left of the second TextBlock:

image

Drag AgreeCount onto PollResultsBar:

image

Select LeftValue from the dropdown:

image

Do the same for RightValue, run the application, and sure enough:

image

Since PollResultsBar is maintaining the value, I tried to hide ProgressBar.Maximum property from Blend:

        private new double Maximum
        {
            set { base.Maximum = value; }
            get { return base.Maximum; }
        }

With no success:

image 

Summary

This seems like a pretty comprehensive way to handle binding to multiple properties in the ViewModel.

I’m pretty sure you could do the same with a behavior,which will probably be my next blog.





Comments are closed.