Learning WPF with BabySmash - MVC or MVP and the Benefits of a Designer
NOTE: If you haven't read the first post in this series, I would encourage you do to that first, or check out the BabySmash category. Also check out http://windowsclient.net/ for more developer info on WPF.
BACKGROUND: This is one of a series of posts on learning WPF. I wrote an application for my 2 year old using WPF, but as I'm a Win32-minded programmer, my working app is full of Win32-isms. It's not a good example of a WPF application even though it uses the technology. I'm calling on community (that's you, Dear Reader) to blog about your solutions to different (horrible) selections of my code. You can get the code http://www.codeplex.com/babysmash. Post your solutions on your blog, in the comments, or in the Issue Tracker and we'll all learn WPF together. Pick a single line, a section, or subsystem or the whole app!
The sheer breadth and depth of WPF is really overwhelming some times. I find myself finding three or four different ways to accomplish basically the same thing and not knowing which one to choose. One wants to balance maintainability and a sense of code aesthetic (Code Smell) as well as focusing on extensibility. All this combined with my own "hacker's impatience" for new features has made messing with BabySmash! a very interesting exercise.
Last week I did a podcast with Felix Corke and Richard Griffin from Conchango on the Developer/Designer relationship and how they navigate their way through the development process and the tools that enable them or hamper their progress.
So far BabySmash has been just basic shapes drawn by me with some help from you, Dear Reader. And let me just say, few of you are designers from the looks of the XAML you've been sending me. ;)
Here's BabySmash before Felix got to the XAML and before I refactored it to support working with him.
Here it is now (and it's got animations and interactions, but you'll have to run it to see):
There's also a bunch of new features that have been made possible by refactoring the project to an MVP (Model-View-Presenter) pattern. But let's start at the beginning.
The Benefits of a Designer and Separated Concerns
I opened my email yesterday and discovered a file sent by Flex called "allshapes1.xaml."
ASIDE: I'm starting to realize that just like regular non-WPF .NET folks can't do everything in Visual Studio, this is especially true when coding in WPF. I'm regularly bouncing between these tools:
- Visual Studio - http://www.msdn.com/vstudio
- Expression Blend - http://www.msdn.com/expression
- KaXAML - http://www.kaxaml.com/
- Snoop - http://www.blois.us/Snoop/
- Mole for Visual Studio - http://karlshifflett.wordpress.com/mole-for-visual-studio/
I busted out KaXAML and was greeted with this!
After I finished crying and wallowing in self pity over my own lack of design skills, I set to figuring out how to exploit benefit from Felix's abilities. When I showed Felix and Richard my initial version of BabySmash last month in Olso, I was using Figures and Shapes and Geometries as my Units of Work. They both suggested I consider UserControls as a better way to work, especially when a Designer gets involved. I mostly ignored them, as I was getting along OK.
However, when Felix's art showed up, it clicked for me. I took each of his shapes and made them UserControls, then modified my factory (FigureGenerator) to make UserControls instead.
UserControls as Unit of Work
The UserControls are self-contained, easy to make from Blend, and as soon as I made one Visual Studio prompted me to reload the project (as Blend and Visual Studio share the SAME csproj/vbproj/sln files) and there it was.
The UserControls compartmentalized everything (duh) nicely so I could start thinking about animations without messing up my business logic or cluttering up my (already cluttered) code.
Adding Animation
Felix drew faces on the shapes, so I asked if he'd include animation. Minutes later he had markup in the XAML to make the eyes blink. Animation in WPF is declarative and time-based (not frame-based). He inserted some key frames and set the animation to repeat forever. Now, the shapes blink occasionally and I didn't have to write any code or worry about threading.
Even better, when I do another animations from code, his animations continue! This means, the shape's faces blink, the shapes fade away after a way, and if you click on them they'll jiggle. Three different animations done in different ways, all happening simultaneously.
Take the Square for example. You start with the basic shape. Notice is has an "x:Name." We can refer to anything with a name later, either in Code or XAML.
<Rectangle x:Name="Body" StrokeThickness="10" Stroke="#ff000000" Width="207" Height="207">
</Rectangle>
Then, he gives it a nice radial fill. Note that he's doing all this work in Blend. I find the XAML building up interesting, myself.
<Rectangle x:Name="Body" StrokeThickness="10" Stroke="#ff000000" Width="207" Height="207">
<Rectangle.Fill>
<RadialGradientBrush MappingMode="Absolute" GradientOrigin="110.185547,455" Center="110.185547,455" RadiusX="98.5" RadiusY="98.5">
<RadialGradientBrush.Transform>
<MatrixTransform Matrix="1,0,-0,-1,-6.685547,558.5" />
</RadialGradientBrush.Transform>
<GradientStop Offset="0" Color="#ffff00ff"/>
<GradientStop Offset="1" Color="#ff9d005c"/>
</RadialGradientBrush>
</Rectangle.Fill>
</Rectangle>
Then he added a face. Note the the Face and Eyes are named.
<Canvas x:Name="Face">
<Canvas x:Name="Eyes">
<Path Fill="#ff000000" Data="...snip..."/>
<Path Fill="#ff000000" Data="...snip..."/>
</Canvas>
<Path Fill="#ff000000" Data="...snip..."/>
</Canvas>
Then he makes the animation a Resource, and sets a Trigger that starts the animation when we're loaded:
<UserControl.Resources>
<Storyboard x:Key="Eyes">
<DoubleAnimationUsingKeyFrames BeginTime="00:00:00"
Storyboard.TargetName="Eyes"
Storyboard.TargetProperty="(UIElement.Opacity)"
RepeatBehavior="Forever">
<SplineDoubleKeyFrame KeyTime="00:00:02.1000000" Value="1"/>
<SplineDoubleKeyFrame KeyTime="00:00:02.1000000" Value="0"/>
<SplineDoubleKeyFrame KeyTime="00:00:02.300000" Value="0"/>
<SplineDoubleKeyFrame KeyTime="00:00:02.300000" Value="1"/>
<SplineDoubleKeyFrame KeyTime="00:00:10.300000" Value="1"/>
</DoubleAnimationUsingKeyFrames>
</Storyboard>
</UserControl.Resources>
<UserControl.Triggers>
<EventTrigger RoutedEvent="FrameworkElement.Loaded">
<BeginStoryboard Storyboard="{StaticResource Eyes}"/>
</EventTrigger>
</UserControl.Triggers>
This background animation all lives in the XAML and works without my code having to do anything.
I wanted an interaction animation. I could have probably done it in XAML with a trigger, but in code it was just this. I took the animations from the WPF AnimationBehaviors project on CodePlex but did them with code rather than in XAML so they can apply to any UserControl including ones that I haven't added yet.
void AllControls_MouseLeftButtonDown(object sender, MouseButtonEventArgs e)
{
UserControl f = sender as UserControl;
if (f != null && f.Opacity > 0.1) //can it be seen?
{
Animation.ApplyRandomAnimationEffect(f, Duration.Automatic);
PlayLaughter();
}
}
Still there are a half-dozen different ways to do things, so I'm still trying to find balance. I can see one going too far left or right and doing everything in XAML or Code even when it's uncomfortable. BabySmash is a hybrid until someone can help me understand better when to do one over the other.
MVP and MultiMonitor
Both Ian Griffiths and Karl Shifflett said I needed to refactor things such that I wasn't putting all the logic in the MainWindow's code behind file. They said that WPF lends itself to an MVP (Model-View-Presenter) pattern, even if you're not really strict about it.
I realized I was going to need to do this soon as my first attempt at Multi-Monitor sucked so bad I ended up yanking it. Initial revisions of BabySmash! had a MainWindow class and all the logic in the code-behind, just like I would have in a WinForms application. The application start up and the Application class would spin through all the Monitors making a window for each one. This had a number of problems:
- The shapes would only appear on the monitor whose Window had focus. You had to change focus with the mouse.
- You could close a Window or two on secondary monitors without closing all of them.
Now, there's a single Controller class that manages as many Windows as it needs to. The app starts up like this:
private void Application_Startup(object sender, StartupEventArgs e)
{
Controller c = new Controller();
c.Launch();
}
And Controller.Launch looks like:
public void Launch()
{
foreach (WinForms.Screen s in WinForms.Screen.AllScreens)
{
MainWindow m = new MainWindow(this)
{
WindowStartupLocation = WindowStartupLocation.Manual,
Left = s.WorkingArea.Left,
Top = s.WorkingArea.Top,
Width = s.WorkingArea.Width,
Height = s.WorkingArea.Height,
WindowStyle = WindowStyle.None,
Topmost = true
};
m.Show();
m.WindowState = WindowState.Maximized;
windows.Add(m);
}
}
Pretty simple to start. I should have smelt that something was wrong with the initial plan as I felt like I was "chasing my tail" in code, trying to get things to work in the original pattern. When I switched to this pattern things just became easy.
Now, Why is this Model View Presenter and not Model View Controller (especially considering I called the class Controller)? Well, Phil does a good job answering this:
With MVC, it’s always the controller’s responsibility to handle mouse and keyboard events. With MVP, GUI components themselves initially handle the user’s input, but delegate to the interpretation of that input to the presenter. This has often been called “Twisting the Triad”, which refers to rotating the three elements of the MVC triangle and replacing the “C” with “P” in order to get MVP.
Now I need to go learn more about Supervising Controller and Passive View as Martin Fowler suggested retiring the MVP pattern in favor of those two variants. The code is still in a sloppy stage (up on CodePlex) but I'd love to have someone (Phil?) who is familiar with pure instances of these patterns to help me tidy it up. I didn't take testing into consideration before (mistake) and I need to get back on the Righteous Path otherwise the technical debt is going to crush me. That's what I get for not going TDD from the start.
The MainWindow's code-behind is just a bunch of small methods that delegate off to the Controller. If there are n MainWindows there's still just the single Controller. MainWindow is full of these kinds of things:
protected override void OnKeyUp(KeyEventArgs e)
{
base.OnKeyUp(e);
e.Handled = true;
controller.ProcessKey(this, e);
}
Every method is less than 20 lines, and most are really simple and boring, which is good.
private void AddFigure(FrameworkElement uie, string s)
{
FigureTemplate template = FigureGenerator.GenerateFigureTemplate(s);
foreach (MainWindow m in this.windows)
{
UserControl f = FigureGenerator.NewUserControlFrom(template);
m.AddUserControl(f);
f.Width = 300;
f.Height = 300;
Canvas.SetLeft(f, Utils.RandomBetweenTwoNumbers(0, Convert.ToInt32(m.ActualWidth - f.Width)));
Canvas.SetTop(f, Utils.RandomBetweenTwoNumbers(0, Convert.ToInt32(m.ActualHeight - f.Height)));
Storyboard storyboard = Animation.CreateDPAnimation(uie, f,
UIElement.OpacityProperty,
new Duration(TimeSpan.FromSeconds(Settings.Default.FadeAfter)));
if (Settings.Default.FadeAway) storyboard.Begin(uie);
IHasFace face = f as IHasFace;
if (face != null)
{
face.FaceVisible = Settings.Default.FacesOnShapes ? Visibility.Visible : Visibility.Hidden;
}
f.MouseLeftButtonDown += HandleMouseLeftButtonDown;
}
FiguresCount++;
PlaySound(template);
}
So far my biggest problems are moving things around, trying to decide "who is responsible for what." Given Animations, Sounds, Shapes, Faces, and all that, where and who is responsible for what, while keeping an eye open for extensibility.
The Little Niceties - Enabling a TextBox if a CheckBox IsChecked
One little aside to end on. Just when I'm getting really pissed at WPF and I'm ready to give up, something simple and cool happens where I realize I'm starting to "get" it.
For example. I've got this Options Dialog that you might remember Jason Kemp refactored. All the controls live inside a Grid and that Grid has a "DataContext" that is my Settings object. All the controls get bound to the object and I don't have to do any loading of values or pulling of values. It just works.
I added that last checkbox and a new option where I wanted to Fade Shapes Away in x seconds. I wanted to disable the TextBox if the Checkbox was not checked. This is the kind of typical operation you might find yourself writing code for in WinForms. You'd hook up events to watch if it's Checked or not, then set the Enabled property of the TextBox, and you also have to watch for the initial load of state. It's not hard in WinForms, but it's irritating, tedious and it's in two places in the code behind.
Even though the DataContext (the thing we are data-binding to) is the Settings object, I can bind objects together by using the ElementName. Check this out. Look at the TextBox's IsEnabled property.
<StackPanel Orientation="Horizontal"
Grid.Row="6" Grid.ColumnSpan="2" HorizontalAlignment="Stretch">
<CheckBox x:Name="FadeChecked" Margin="15,0,0,0"
IsChecked="{Binding Path=Default.FadeAway,Mode=TwoWay}" >
Fade Shapes Away in</CheckBox>
<TextBox Margin="5,0,0,0"
Text="{Binding Path=Default.FadeAfter}"
IsEnabled="{Binding ElementName=FadeChecked,Path=IsChecked,Mode=TwoWay}"
Height="20" Width="25" />
<TextBlock>secs.</TextBlock>
</StackPanel>
It's a tiny victory, but it made me happy.
Related Links
- BabySmash - http://www.babysmash.com
- All the BabySmash articles so far
- Introducing BabySmash - A WPF Experiment
- Learning WPF with BabySmash - Configuration with DataBinding
- Learning WPF with BabySmash - Speech Synthesis
- Learning WPF with BabySmash - Keeping it DRY with XAML Styles
- Learning WPF with BabySmash - Pushing things up a level with another set of eyes
- Learning WPF with BabySmash - Factories, Interfaces, Delegates and Lambdas, oh my!
About Scott
Scott Hanselman is a former professor, former Chief Architect in finance, now speaker, consultant, father, diabetic, and Microsoft employee. He is a failed stand-up comic, a cornrower, and a book author.
About Newsletter
Bryan
In fact, dependency properties could be used to extend controls behavior the same way extension methods can extend class behavior. For example, I have a dependency property that enables mouse dragging for any Canvas child.
This approach allows you to have several different animations encapsulated in that behavior and to choose which one you want for particular control at the moment of attaching by just setting the right value for the property. Even more, this would allow your designer to select the right animation from a set of animations she built in the XAML without you changing the underlying code.
textBox1.DataBindings.Add("Enabled", checkBox1, "Checked");Not as slick or comprehensive as the WPF binding stuff, but once you're exposed to it it gets you thinking about things differently when you're restricted to WinForms.
Franci - Is this http://www.codeproject.com/KB/WPF/wpf_worldclocks.aspx an example of what you're saying? Would you mind looking at the BabySmash code and showing me?
Duncan - Good point!
Has your kid (and you of course) already played with BabySmash!? How was the experience? I'm curious to know if both your kids are attracted to this app, or just the younger one.
You are binding the enabled property of a textbox to the checked property of a checkbox. This way you are putting business logic inside of your xaml and that is not a good practice. You should definitely look into the concept of a viewmodel to which you bind. The viewmodel is an interpretation of the MVC/MVP framework and can be seen as a layer between your UI and your model. By binding your UI to the viewmodel, instead of directly to the model, you can bind properties like IsEnabled of the textbox to a new property on your viewmodel. This way, you can determine the logic of when the textbox should be enabled inside of the VM.
Think about it, Xaml should be used to declare your UI, _not_ to implement your logic.
I agree UI and business logic should be kept separate, however I feel that in this case it has.
I will admit I don't know much about MVC/MVP Models so this could just be my misunderstanding.
The way IsEnabled has bee used here it has no effect on the operation of the program or its intended outcome's. Nether are there any cases were the business logic would change the state of this UI element.
Here are two questions to think about.
If you delete that one line of XAML what is the net effect on the Program?
However if you are setting the IsEnabled through code what would be the effect on the program if you changed the check box and text box for a custom user control?
(Perhaps one would want to use the same control for "Clear after x shapes")
Or for that matter If you changed the XAML completely for the Dialog would be the effect on the program if the IsEnabled was set through code?
In this case the IsEnabled is being used as a visual effect, so setting it through code would Bind the business logic to the UI rather than binding the UI to the business logic.
Maybe I don't get this MVC/MVP thing.
I am still learning after all.
Let me try to restate Ruurd's point. The fact that the FadeAfter property is only relevant if the FadeAway property is true (which is why we are disabling it if the checkbox is unchecked) is a "business rule" (or whatever term you want to use in a non-business application). By creating the Binding the way Scott has, he is essentially recreating this business rule in his presentation; if that rule ever changes he will have to change his presentation. A better way would be to create a property in the options class called, I don't know, NeedFadeAfter, which could then be bound to the IsEnabled property of the TextBox. In this case, NeedFadeAfter would reflect FadeAway, but theoretically that "rule" could change at any time, and we would only have to change the "business logic", instead of the presentation.
I have to say that its a pretty nitpicky point for an options dialog, but strictly speaking I agree with it.
keep up the great articles Scott!
While I haven't ported it to WPF, yet, my open source project Update Controls .NET can do this in WinForms. It looks like this:
private bool fadeCheckBox_GetChecked()
{
return _options.Fade;
}
private void fadeCheckBox_SetChecked(bool value)
{
_options.Fade = value;
}
private bool fadeSecondsTextBox_GetEnabled()
{
return _options.Fade;
}
private string fadeSecondsTextBox_GetText()
{
return _options.FadeSeconds.ToString();
}
private void fadeSecondsTextBox_SetText(string value)
{
_options.FadeSeconds = int.Parse(value);
}
Comments are closed.