Scott Hanselman introduced us to BabySmash today. It’s an app for his kids that he developed using his arcane Win32 knowledge in a WPF app. He did it on purpose, mind you, to teach himself WPF, with us watching.
I think this is a fantastic idea. When I was writing the AutoComplete TextBox series, I thought it would be a cool idea for all the major WPF book authors (Adam Nathan, Chris Sells & Ian Griffiths, Charles Petzold and Chris Anderson) to write their implementations of the same problem. They’re all very good at explaining, they’d all come up with different solutions and we could all figure out the best way to do things in WPF, as Scott alludes to in his post on BabySmash.
I’ve taken a look at the code and, boy, it needs work. I saw immediately something I’ve experienced pain with before, which I’ll talk about in this post: custom settings. In .NET 2.0, one of the cool unsung heroes is the settings code in System.Configuration. Note that it’s not tied to WPF or WinForms. With it, you’re able to have user- or app-scoped settings. They can be local or roamable. The framework takes care of storage, although it’s extensible. Visual Studio assists with code-gen so you can access your settings with code.
By default, VS will create a Settings class that inherits from ApplicationSettingsBase in the YourProjectNamespace.Properties namespace. The settings you create in the Settings designer get properties generated for them. The Settings class also has a static instance accessed through the Default property. Scott actually created this file, but then didn’t use it. I haven’t used ClickOnce, but I just can’t see Microsoft not supporting the settings file automagically in ClickOnce.
Pictured below is BabySmash’s settings with Scott’s intended properties filled in.
So, like I said, this is independent of UI framework. How do we use it in WPF? Databinding! By far the best thing of WPF, even though you have to put some time in to learn it.
Before I show the code to databind to the application settings, allow me to show you the XAML for the Options page in BabySmash.
<Window x:Class="BabySmash.Options" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" Title="Baby Smash! - Options" Unloaded="Window_Unloaded" Loaded="Window_Loaded" Height="188" Width="268" ShowInTaskbar="False" Topmost="True" WindowStartupLocation="CenterScreen" WindowStyle="ThreeDBorderWindow" ResizeMode="NoResize"> <Grid> <Button Height="23" Name="OK" VerticalAlignment="Bottom" Click="OK_Click" IsDefault="True" Margin="64,0,102,9">OK</Button> <Button Height="23" Margin="0,0,8,9" Name="Cancel" VerticalAlignment="Bottom" IsCancel="True" HorizontalAlignment="Right" Width="80" Click="Cancel_Click">Cancel</Button> <Label Height="28" Margin="14,19,0,0" Name="lblClearAfter" VerticalAlignment="Top" HorizontalAlignment="Left" Width="120">Clear After x Shapes</Label> <TextBox Height="23" Margin="0,19,8,0" Name="txtClearAfter" VerticalAlignment="Top" HorizontalAlignment="Right" Width="101"></TextBox> <CheckBox Margin="0,81,0,0" Name="chkForceUppercase" Height="16" HorizontalAlignment="Right" VerticalAlignment="Top" Width="109" >Force Uppercase</CheckBox> <ComboBox Margin="0,48,8,0" Name="cmbSound" Height="23" HorizontalAlignment="Right" VerticalAlignment="Top" Width="101" IsDropDownOpen="False"> <ComboBoxItem>None</ComboBoxItem> <ComboBoxItem>Speech</ComboBoxItem> <ComboBoxItem>Laughter</ComboBoxItem> </ComboBox> <Label Height="28" Margin="14,43,112,0" Name="label1" VerticalAlignment="Top">Sounds</Label> </Grid> </Window>
If you know XAML and how the WPF grid works, then you know that this is really bad code. This is probably the most egregious thing about WPF, IMO: the visual designer. Scott didn’t write this code. Visual Studio did.
Petzold has a great transcript/paper of a talk he gave to a New York user group in 2005 titled, Does Visual Studio Rot the Mind? In it he talks about the evolution of visual programming and Visual Studio. It’s written in 2005, before WPF even got its name, but while he was writing his book, Applications = Code + Markup. But the points he raises are still valid. WPF is, by default, laid out without reference to pixels. The intention of its designers was that with WPF, you say what you want, the WPF figures out how to make it so. Most of the new frameworks from Microsoft have that in their design: "tell me what, I’ll figure out how."
In WPF’s case, it’s completely undermined by the visual designer in Visual Studio. Note above the code snippet from Scott’s Options page: see all those Margin attributes? That’s like hard-coding the same string value in more than one place. This is a dialog box, but humour me. Suppose we allowed this dialog to be resized by the user. What would happen? Chaos.
This is what Visual Studio gives you out of the box. Shameful. And if you want to use databinding, forget it, you have to write the XAML by hand. I am dimly aware of Expression Blend; I’ve used it a few times and I think I remember it being a little smarter than Visual Studio. I think that shows how Microsoft is a little out of touch with developers outside of Microsoft. In theory, I like the idea that developers can work on the app doing something, while designers work on the app’s appearance. Really good idea, in theory. In my experience, the developer is the designer the majority of the time. In this scenario, Visual Studio needs a lot more work.
Anyway, let’s stop ranting. I also think it’s a developer’s responsibility to learn either how technology is used if it’s well-established, in the case, of, say, PHP or ASP.NET; or how its designers intended it to be used in the case of WPF. That’s what I’ve done, and what I’m still doing now. So let’s make an Options window that binds to the app settings. Scott’s was laid out, as he intended it, as the following:
There are plenty of ways to do this with the default panel types in WPF. I’ll use the Grid since it’s the most powerful, the default choice and hence the most popular. I’ll preserve as much of what Scott had, and try to make it pixel perfect.
OK, I’m back. Did you miss me? Here’s what I have:
<Window x:Class="WpfApplication1.Window1" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:local="clr-namespace:WpfApplication1.Properties" xmlns:l="clr-namespace:WpfApplication1" Title="Baby Smash! - Options" Height="188" Width="268" ShowInTaskbar="False" Topmost="True" WindowStartupLocation="CenterScreen" WindowStyle="ThreeDBorderWindow" ResizeMode="NoResize"> <Window.Resources> <local:Settings x:Key="settings" /> </Window.Resources> <Grid DataContext="{StaticResource settings}"> <Grid.ColumnDefinitions> <ColumnDefinition Width="*" /> <ColumnDefinition Width="*" /> </Grid.ColumnDefinitions> <Grid.RowDefinitions> <RowDefinition Height="Auto" /> <RowDefinition Height="Auto" /> <RowDefinition Height="*" /> <RowDefinition Height="Auto" /> </Grid.RowDefinitions> <Label Height="23" Margin="10,20,0,0" Grid.ColumnSpan="2">Clear after x Shape</Label> <TextBox Text="{Binding Path=Default.ClearAfter}" Height="23" Grid.Column="1" Margin="15,20,0,0"/> <Label Height="23" Grid.Row="1" Margin="10">Sounds</Label> <ComboBox Grid.Column="1" Grid.Row="1" Height="23" Margin="15,0,0,0"> <ComboBoxItem >None</ComboBoxItem> <ComboBoxItem >Laughter</ComboBoxItem> <ComboBoxItem >Speech</ComboBoxItem> </ComboBox> <CheckBox Grid.Row="2" Grid.Column="1" Margin="15,0,0,0" IsChecked="{Binding Path=Default.ForceUppercase,Mode=TwoWay}" > Force Uppercase </CheckBox> <StackPanel Orientation="Horizontal" Grid.Row="3" Grid.ColumnSpan="2" HorizontalAlignment="Right"> <Button Name="okButton" IsDefault="True" Margin="0,7,10,7" Padding="30,0,30,0" Click="okButton_Click">OK</Button> <Button IsCancel="True" Margin="5,7,7,7" Padding="15,0,15,0">Cancel</Button> </StackPanel> </Grid> </Window>
It looks more verbose for sure, but I think that’s more because of editing by hand and using the ENTER key. Let’s pull out unnecessary stuff and show the code that binds to the application settings
<Window x:Class="WpfApplication1.Window1" xmlns:local="clr-namespace:WpfApplication1.Properties" xmlns:l="clr-namespace:WpfApplication1" > <Window.Resources> <local:Settings x:Key="settings" /> </Window.Resources> <Grid DataContext="{StaticResource settings}"> <TextBox Text="{Binding Path=Default.ClearAfter}" /> <CheckBox IsChecked="{Binding Path=Default.ForceUppercase}" > Force Uppercase </CheckBox> <StackPanel > <Button Name="okButton" IsDefault="True" Margin="0,7,10,7" Padding="30,0,30,0" Click="okButton_Click">OK</Button> ... </StackPanel> </Grid> </Window>
There are some concepts you’ll see over and over again WPF: the use of ResourceDictionaries (Window.Resources in the XAML), DataContext, and DataBindings. I won’t explain all of it in detail (this post is long as is), but I’ll briefly explain it and bold terms that you can search the internets for more info.
Most of the XAML should be familiar even if you’ve never seen it before. It’s fairly close to ASP.NET code, if memory serves, and the XML hierarchy is a good fit for a model that represents a window as a nested tree of controls. The Windows.Resources element adds a Settings object to the window’s ResourceDictionary. All resource dictionary items must have a key to refer to. We set the DataContext on the Grid to the settings object declared in the resources. It’s a StaticResource which means that WPF loads it once and stops listening to updates to that resource. The DataContext is essentially associating the Settings object with the Grid. The Binding on the TextBox’s Text property is BindingExpression whose Path points to the Default.ClearAfter property of the Settings object. That’s a little advanced, but hey, it’s "real world." And that’s all you have to do to get the settings to show up in the right controls. Notice there is no procedural code.
There’s one more thing to talk about before I quit. The Settings object’s properties change when you change the values in the controls (set the TextBox to 32, and ClearAfter is set to 32), but we have to tell the Settings object that we want to keep those values. We do that in okButton’s handler:
private void okButton_Click(object sender, RoutedEventArgs e) { Properties.Settings.Default.Save(); this.Close(); }
Too easy, eh?
There are some caveats because I didn’t test thoroughly: databinding by default updates the source after focus changes so if you set the TextBox to 32, then click OK, it may not update the property properly as you’d expect. I explicitly left the third property in settings to be set by you if you want to take up the challenge. I added the settings in BabySmash but they weren’t showing up properly. I don’t know why and I didn’t pursue.
So there you have it. I didn’t show Scott’s Option dialog code, but I’ve significantly reduced it. My Options Dialog window XAML code may be a little longer but I get some layout benefits by using the Grid to its potential. Of course, you may do something different, and it may be better, but that’s the whole point of the Scott’s app. There are some treats in the solution below. I got tired of writing this post before I covered everything that I noticed. I’m going to play video games now.
17 Replies to “Redoing the Options Dialog in BabySmash”
Comments are closed.
Fantastic! I’ll add this to the app and blog about the solution.
I figured out the Combobox databinding. The key was SelectdeValuePath. It’s required to explain to the box what value in the combo we’re using to bind on.
None
Laughter
Speech
Cool!
Some things I’d be interested in followups for:
1) “databinding by default updates the source after focus changes so if you set the TextBox to 32, then click OK, it may not update the property properly as you’d expect.” This seems like a fairly serious problem. How is this usually handled in WPF apps?
2) Could you give a sample of how to lay out the option dialog, if you weren’t trying to be pixel-perfect? I.e., it sounds to me like XAML should let you just say “I want these three settings aligned kinda like so,” as one does in XHTML, and get nice results. Is this true? How would it work? It seems like it would be superior, and thus a good thing to contribute for the purposes of the overall project goal.
Thanks!! This is a fun project to watch, as I feel exactly like Scott about WPF. (Well, except I’m stuck in a Windows.Forms mindset, not a Win32 mindset, so at least I’m not a complete neanderthal).
@Domenic Denicola: I’m a WinForms dev too. If you know some of the advanced topics there, like databinding. Then picking up WPF to be productive in it should be fairly quick, say a month of dabbling. I’m no expert in WPF, and I have to check syntax still.
The thing about WPF is it’s like HTML/CSS, it’s like Windows Forms, it’s like WebForms but not quite enough so you have to have those three in the back of your mind until it all clicks. I think it’s clicked for me, finally.
I’ll take a look at those followups.
Great post, very well explained.
I have a question. Suppose you were doing something very similar with a dialog that needed to databind to properties on a data class, but the data class wasn’t the Settings file. How would you do that?
I’m specifically thinking about a scenario where someone would be opening a properties form from a Grid. Typically you’d fill a data class with details for the row they selected and pass that to the properties form.
Since I need to pass in the data class, I’m not sure how to databind to it in XAML because I don’t know how to treat it as a static resource. Could you point me in the right direction?
@Kevin Berridge: Thanks, Kevin.
I’ll follow up in a post, because I see some confusion on Scott’s part about that stuff too. The short answer is yes, you can absolutely do that. In fact that’s a better demo of the DataContext concept than using the Settings class.
If you can’t wait, try this: with your dialog class, pass in your data class in the constructor then set this.DataContext (the dialog’s DataContext) to your data class. In the XAML that declares your window, set all the controls with databinding as required putting in the right Paths. Then, when you run, it should load the data.
I’ll post about this soon, hopefully today.
Wow, that is so bloody simple… I should have seen it coming.
I assume if you had 2 containers on the window that needed to have 2 different DataContexts (if you were passing in more than one data class) that you’d just set the DataContext directly on those containers.
Thanks so much!
Your rant on Blend is off-base. They have vastly improved Blend recently and databinding is fully supported (has been for a while). It very much supports designer-developer interaction allowing on my current project and the past 2 before it to have a designer with limited C# knowledge to be very productive and produce a much more kick-ass GUI than anything a developer would produce. I’m not knocking you as a GUI guy, but my guess is a designer would do better.
That said there are still some glitches in Blend. Specifically adding a new file does not get the file checked into TFS properly. Sigh.
@Darrell: My rant wasn’t against Blend; it was against Visual Studio 2008. My feeling is that even if there were a developer and designer collaboration, the developer would be responsible for hooking up data bindings.
But why can’t he do it from the visual designer?
I wasn’t disparaging the collaboration, either. I think that’s great that designers are getting tools that let them design something we can use right in the app; my observation is that it’s rare to have a designer nearby. In my experience, which may be limited, or in the minority, the developers responsible for how the app looks as well as how it works.
Thanks for the great explanation but what happens when a user modifies one of these databound entries and then selects the Cancel button in the dialog?
Wouldn’t the programmer also need to do a Properties.Settings.Default.Reload(); in the OnCancel handler?
@Joe: Yes, you’re right. That is something I neglected to mention.
For the property not getting changed until you lose focus, you should be able to add the following:
UpdateSourceTrigger=PropertyChanged
so you should have:
I haven’t tested this in your sample, but I am using this method when I’m binding a TextBox to a CLR object property and it works great.
I hope this helps.
John
MuvEnum