For background on what BabySmash is, read Scott Hanselman’s post here.
This post was inspired by Ian Griffiths’s logged issue in the BabySmash issue tracker, titled "Application Logic Does Not Belong in Code Behind." My gut reaction was that that was overkill for such a simple application. BabySmash has almost no logic to it; it basically take what’s pressed and shows it on screen. I was still going to write a post about the technique because of its relevance to any app that displays data; I think databound list controls should be the most common WPF idiom, it’s so important. But, something still rankled about going so far for such a simple concept. So, I went through with the refactoring, and I gotta tell you: Ian was absolutely right.
You should read Ian’s description one of the problems with the BabySmash code. He also describes a solution, but if you aren’t familiar with the ItemsControl, most of it will go right over your head (hopefully not after this post though). I think it’s best if we describe BabySmash in a way that will help us move to Ian’s solution. Databinding in WPF, especially list-bound data, is where WPF diverges from the UI technologies that inspired it. It’s not immediately obvious with BabySmash’s raison d’etre that we can make it data-bound; I’ve found, however, that once you know about WPF’s databinding, you want to model your domain classes to take advantage of it. That’s how I approached changing BabySmash this time. Mike Hillberg has a good post on writing the domain model with WPF.
One more thing before I begin. Modelling any domain problem is subject to opinion. Software design is a matter of trade offs. You may prioritize different parts of the problem than I. Talking about ItemsControl requires a data model. I’ll talk about the model I chose. Hopefully, if you disagree with my object model, the technique of using an ItemsControl I show here can still applied to your model. I’m also not going to dwell too long on all the whys of my object model, although that’s an interesting topic as well. I welcome feedback or those why questions in the comments.
Where’s the data?
Allow me to distill BabySmash down to requirements language:
When a user presses a key in BabySmash, then, depending on the key pressed, a shape, letter or number will be pressed:
- if the key is a letter key (A through Z), then that letter is displayed on screen in a random position with a random colour;
- if the key is a number, the behaviour is the same as for letters; and
- if the key is anything else, a shape is chosen at random and displayed on screen in a random position with a random colour
The shapes drawn are currently square, circle, triangle and star.
Depending on a user setting, as each key is pressed, either laughter or speech is heard. If speech is chosen, then the shape or letter put on screen is announced.
How’s that?
I’ve written it down that way so we can "see where the classes are." Good domain design is all about getting the class model right. Looking at the above description, I can see a few good candidate classes for the essential BabySmash: shape (including square, circle, triangle and star), letter, number, position, colour and sound.
We can break these down further into two categories: display and data. The colour, sound and position have to do with display. The "data" is what shape to draw: the letter, number, or shape dictated by the key press. I’d say the essence of BabySmash is the data category. Below is the class definition that I came up with for that concept:
public abstract class Figure { private UIElement shape; private readonly string name; protected Figure(Brush fill, string name) { this.name = name; } public UIElement Shape { get { return shape; } protected set { shape = value; } } public string Name { get { return name; } } }
My abstract Figure class has a Shape, typed to UIElement, associated with it and a name. In the constructor, I pass the Brush that will fill the shape. This allows me to create arbitrarily complex shapes to display; here are a couple Figure classes that inherit from Figure:
public class LetterFigure : Figure { public LetterFigure(Brush fill, string name) : base(fill, name) { string nameToDisplay; if (Properties.Settings.Default.ForceUppercase) { nameToDisplay = name; } else { if (Utils.GetRandomBoolean()) nameToDisplay = name; else nameToDisplay = name.ToLowerInvariant(); } Shape = Utils.DrawCharacter(400, nameToDisplay, fill); } } public class SquareFigure : Figure { public SquareFigure(Brush fill) : base(fill, "square") { Shape = new Rectangle() { Fill = fill, Height = 380, Width = 380, StrokeThickness = 5, Stroke = Brushes.Black, }; } }
Basically, I took the code from Window1.xaml.cs in the BabySmash solution that creates the shapes and broke it out into classes. There are also CircleFigure, StarFigure and TriangleFigure that are similar to SquareFigure above. I probably could move the Utils.DrawCharacter() code into the LetterFigure constructor as a further refactoring.
We also need a way to create Figures based on input from the user. That class is below:
public class FigureGenerator { private int clearAfter; private readonly ObservableCollection<Figure> figures = new ObservableCollection<Figure>(); public int ClearAfter { get { return clearAfter; } set { clearAfter = value; } } public ObservableCollection<Figure> Figures { get { return figures; } } public void Generate(string letter) { if (figures.Count == clearAfter) figures.Clear(); figures.Add(GenerateFigure(letter)); } private Figure GenerateFigure(string letter) { //TODO: Should this be in XAML? Would that make it better? Brush fill = Utils.GetRandomColoredBrush(); if (letter.Length == 1 && Char.IsLetterOrDigit(letter[0])) { return new LetterFigure(fill, letter); } else { int shape = Utils.RandomBetweenTwoNumbers(0, 3); //TODO: Should I change the height, width and stroke to be relative to the screen size? //TODO: I think I need a shapefactory? switch (shape) { case 0: return new SquareFigure(fill); case 1: return new CircleFigure(fill); case 2: return new TriangleFigure(fill); case 3: return new StarFigure(fill); } } return null; } }
Again, you can see by the TODOs, I just took Scott’s code and moved it around. The FigureGenerator class holds onto the Figures collection that will be used in the UI as the data source. The Figures collection is typed to ObservableCollection<Figure>. This is one of those magic classes offered by WPF that can track changes, so adding a figure to the collection adds an element on screen. FigureGenerator also takes input from the Window class through the Generate() method.
Consuming the Data
OK, we’ve got our data model. Now we want to display it. I’m taking Ian’s recommendations, namely using an ItemsControl,. I’ll break this down into steps.
The first thing is to add the ItemsControl to the Window. We’ll do that in XAML. At the same time I’ll declare my FigureGenerator that will generate the figures. Here’s the XAML first:
<Window> <Window.Resources> <local:FigureGenerator x:Key="figureGenerator"/> </Window.Resources> <Grid Name="grid"> <ItemsControl Name="figures" ItemsSource="{Binding Source={StaticResource figureGenerator},Path=Figures}"> </ItemsControl> </Grid> </Window>
I spoke about StaticResources before. Essentially, what I’m doing here is adding a FigureGenerator instance to the Window’s resources. Then, I’m binding the ItemsControl to the Figures property on the FigureGenerator. There is one more thing I have to do in the code-behind. When a key is pressed, I have to tell the FigureGenerator instance about it. Here are the relevant bits from the code behind:
private FigureGenerator figureGenerator; public Window1() { objSpeech = new SpVoice(); InitializeComponent(); figureGenerator = (FigureGenerator) this.Resources["figureGenerator"]; figureGenerator.ClearAfter = Properties.Settings.Default.ClearAfter; } private void Window_KeyUp(object sender, System.Windows.Input.KeyEventArgs e) { PlayLaughter(); string s = e.Key.ToString(); if (s.Length == 2 && s[0] == 'D') s = s[1].ToString(); //HACK: WTF? Numbers start with a "D?" figureGenerator.Generate(s); }
You can run BabySmash now, but what you’ll see isn’t that entertaining:
What’s happening here? We’re using an ItemsControl with the default templates, which just shows each item in the collection using it’s ToString() method; the items are contained in a StackPanel. In Figure’s case, since we didn’t override it, uses Object.ToString(), which prints the type name. Let’s fix that.
The ItemsControl is an important control. It’s the basis for all of the collection controls: TreeView, ListView, ListBox, TabControl, even ComboBox and Menu. All those classes use the behaviour inherited from ItemsControl to display themselves in obviously different ways; i.e. they’re all special cases of the basic ItemsControl (to a first approximation anyway, I’m glossing over some important details). We’re going to show how to customize the look of an ItemsControl in the same way that the above controls do it.
In the XAML above, we told the ItemsControl where our data is, but we haven’t told the ItemsControl how to display it. We do that with Templates. Every control in WPF has a Template that dictates the look of the control. WPF introduces the concept of a lookless control; a control whose appearance is independent from behaviour. The best example of the lookless control is the button. What shape the button takes is independent of what makes a button a button, namely that you can click it. It’s roughly analogous to how CSS affects elements’ appearance in HTML. Typically, a control’s appearance can be modified by replacing it’s ControlTemplate via the Template property. ItemsControl is a little bit different though.
With ItemsControls, we can manipulate independent parts of ItemsControls with different templates. The template you’ll use most often in your apps is the ItemTemplate. The ItemTemplate property is typed to DataTemplate, which as the name suggests can be used to template your data, which can be of any type. This is a very powerful concept. It allows us to display any old type with the same ItemsControl, something pretty much impossible with Windows Forms or Win32 or anything else. We get infinite custom controls.
Let’s make an ItemTemplate for BabySmash. Most of it is already done for us. Scott made the basic shapes, I just moved them into different classes. The thing we want to display can be accessed by the Shape property on the Figure class. Now we want to tell WPF to display that. We do that with the magic ContentPresenter. The ContentPresenter presents whatever you give it the best way it can. Pass it a Button, it’ll display that button. Pass it a string, it’ll print a TextBlock with the string as Text; pass it an object, it’ll print a TextBlock with the string from its ToString() as Text. That’s right, we’ve been using a ContentPresenter all along. We have the shape we just have to point to it.
<ItemsControl Name="figures" ItemsSource="{Binding Source={StaticResource figureGenerator},Path=Figures}"> <ItemsControl.ItemTemplate> <DataTemplate> <ContentPresenter Content="{Binding Path=Shape}" /> </DataTemplate> </ItemsControl.ItemTemplate> </ItemsControl>
Let’s run it again:
Looking better.
Now we want those letters to show up all over the screen. To do that requires two changes. The first, we have to change the underlying Panel that our ItemsControl uses to layout its Items. I mentioned above that ItemsControl uses a StackPanel by default. We’re concerned with position, therefore we only have one choice: Canvas. We do this by setting the ItemsPanel property to an ItemsPanelTemplate object that contains a Canvas:
<ItemsControl Name="figures" ItemsSource="{Binding Source={StaticResource figureGenerator},Path=Figures}"> <ItemsControl.ItemTemplate> <DataTemplate> <ContentPresenter Content="{Binding Path=Shape}" /> </DataTemplate> </ItemsControl.ItemTemplate> <ItemsControl.ItemsPanel> <ItemsPanelTemplate> <Canvas /> </ItemsPanelTemplate> </ItemsControl.ItemsPanel> </ItemsControl>
If you ran it now, you wouldn’t see anything different. That’s because we’re not setting the Canvas.Left and Canvas.Top attached properties on the shapes yet. You may think we should do that on the ItemTemplate above; I know I did when I was figuring this out a few weeks ago (I tried this same trick of using an ItemsControl for a line graph, a subject for a future post). Well, you’d be wrong.
There’s a third aspect of the ItemsControl: the item container. The item container is responsible for selection management of that item in the collection. There is no specific class hierarchy for it, but if you look around in Reflector, you’ll see that each ItemsControl child (like ListBox, ComboBox, etc) has an override for the GetContainerItemOverride() and returns its item container class (whose name follows from the control with which its associated: ListBoxItem for ListBox and so on). If you inspect the Visual Tree, you’ll see that the actual items in the ItemsControl are the associated item container. It’s this container that we want to manipulate to place the item. We do that with the ItemContainerStyle property on the ItemsControl. Incidently, ItemsControl uses FrameworkElement as it’s container.
<ItemsControl Name="figures" ItemsSource="{Binding Source={StaticResource figureGenerator},Path=Figures}"> <ItemsControl.ItemTemplate> <DataTemplate> <ContentPresenter Content="{Binding Path=Shape}" /> </DataTemplate> </ItemsControl.ItemTemplate> <ItemsControl.ItemsPanel> <ItemsPanelTemplate> <Canvas /> </ItemsPanelTemplate> </ItemsControl.ItemsPanel> <ItemsControl.ItemContainerStyle> <Style> <Setter Property="Canvas.Left" Value="{Binding RelativeSource={RelativeSource FindAncestor, AncestorType={x:Type local:Window1}},Path=ShapeLeft}"/> <Setter Property="Canvas.Top" Value="{Binding RelativeSource={RelativeSource FindAncestor, AncestorType={x:Type local:Window1}},Path=ShapeTop}"/> </Style> </ItemsControl.ItemContainerStyle> </ItemsControl>
This is the entire ItemsControl. Note there are many ways that occurred to me to create the random numbers for Top and Left. I thought the simplest would be to add two properties on the Window1 class, and then bind to them using a syntax you probably haven’t seen before. This method traverses the XAML (more technically, the visual or logical tree) directly to get to the data source. I’m telling WPF to find the first Window1 class instance in the tree, then bind to either ShapeLeft or ShapeTop properties. Those properties generate random doubles and consist of Scott’s code cut and pasted.
One more thing to add back and we’re done.
If the user chooses Speech as the option for sound, a computer voice is supposed to speak the letter or shape that was just shown. We got rid of that when I rewrote the Window_KeyUp method above. Let’s add it back.
When you set the ItemsSource property on an ItemsControl, the ItemsControl does one more indirection on you. It wraps the source in an ICollectionView. The ICollectionView monitors the collection for changes and handles currency for the ItemsControl. The ListBox, or any other ItemsControl, is completely ignorant of the currently selected item, and so is the source collection (which is typed to IEnumerable<T>). This separation allows for synchronizing multiple controls on the same collection, so that if the selection changes all controls are updated. The ICollectionView is associated with the source, not the targets. The cool thing is, we can access it directly; filtering, grouping and sorting are all accomplished with the collection view (something I use to make an autocomplete text box in WPF). So, we’ll use that to be notified of any collection additions and play the appropriate speech based on the item.
public Window1() { objSpeech = new SpVoice(); InitializeComponent(); figureGenerator = (FigureGenerator) this.Resources["figureGenerator"]; figureGenerator.ClearAfter = Properties.Settings.Default.ClearAfter; ICollectionView collectionView = CollectionViewSource.GetDefaultView(figureGenerator.Figures); collectionView.CollectionChanged += FiguresCollectionChanged; } private void FiguresCollectionChanged(object sender, NotifyCollectionChangedEventArgs e) { if (e.Action == NotifyCollectionChangedAction.Add) { Figure f = (Figure) e.NewItems[0]; SpeakString(f.Name); } }
Hit F5. BabySmash is back in all its glory!
So was this worth it?
Before I did this, Window1.xaml.cs weighed in at about 230 lines. Now it’s a svelte 125 lines, but I added about 180 lines with that Figure stuff! I’m not even including the XAML either.
Still, I think I reduced the cognitive load of the app. By putting the display logic in XAML, I noticed that I could concentrate more on what the app was doing. Before, it was all code, so you didn’t really know where to look.
Now, we only need concern ourselves with one idea, Figure, when we’re thinking about BabySmash in general. With a little more work, we can add other shapes as the kids get older like dodecahedron; or animal shapes or pictures of family. Score one for extensibility.
We’ve also somewhat separated the appearance of the shapes from the shapes themselves. Score one for encapsulation.
We can also test the FigureGenerator and Figure classes without a UI. Score one for automated testing.
Hopefully, I’ve shown how the ItemsControl provides huge benefits for displaying your domain here. If you want to play around some more, I suggest using ListBox with a custom ItemTemplate to get a feel for how it works.
Interesting. I’m starting to explore WPF and it looks identical to Flash from version 6+ when approached by your ideas.
What I don’t understand is how so many bloggers are saying “Look, you can put all of your binding logic into the XAML, where it’ll be really obfuscated and difficult to read and not unit testable. but it’ll be cool!”
Thanks for showing this example.
I intend to pursue a your more mindful approach to databinding.
Cheers
ben
So, how is this approach effected now that BabySmash is using Designer provide shapes defined in seperate .xaml files.
Regardless, the was a very well written post and I learned some new concept. Thanks 🙂
Todd,
Glad you liked it..
I wrote that in the early days before Hanselman took it in a different direction. I haven’t looked at BabySmash in months. I’m not sure anything I submitted is still in the project.