Scott Hanselman

Learning WPF with BabySmash - Configuration with DataBinding

June 03, 2008 Comment on this post [19] Posted in BabySmash | Windows Client | WPF
Sponsored By

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 first blogger to take me up on this was Jason Kemp, and his resulting post called "Redoing the Options Dialog in BabySmash" is a model for how to expand the knowledge of the community.

Jason said "I’ve taken a look at the code and, boy, it needs work." Heh. I warned you. He decided to tackle two related areas, the Options Dialog's XAML layout and the ConfigurationManager settings subsystem. We'll tackle the underlying Configuration for this post.

WPF Configuration Done Wrong/Poorly

BabySmash stores configuration info, and I wanted to use IsolatedStorage, where the system picks a file- and path-name and you write to it. I search the web and IsolatedStorage made easy was an attractive first hit. I copy/pasted the code and hooked it up through the app and it worked great.

It was pretty cheesy though. First, it was a big custom class written in 2004 that did a bunch of XML work to save your settings. But, it was hidden in a class and worked, so I didn't sweat it.

The part that really smelled bad was the juggling back and forth between three places. The options were in a file, they were in instance variables, and they were in the controls in the Options dialog. This is how it was down in Win32. It was straightforward moving the values around but tedious.

The options dialog loaded and saved the settings using the class...

private void OK_Click(object sender, RoutedEventArgs e)
{
config.Write("ClearAfter", txtClearAfter.Text);
config.Write("ForceUppercase", chkForceUppercase.IsChecked.ToString());
config.Write("Sounds", cmbSound.Text);
config.Persist();
this.Close();
}

private void Window_Loaded(object sender, RoutedEventArgs e)
{
config = IsolatedStorage.ConfigurationManager.GetConfigurationManager("BabySmash");
txtClearAfter.Text = config.ReadInteger("ClearAfter", 20).ToString();
chkForceUppercase.IsChecked = config.ReadBoolean("ForceUppercase", true);
cmbSound.Text = config.Read("Sounds", "Laughter");
}

Then later in the main Window I'd load it into local variables:

private void LoadConfigSettings()
{
//TODO: This is duplicated elsewhere
config = IsolatedStorage.ConfigurationManager.GetConfigurationManager("BabySmash");
clearAfter = config.ReadInteger("ClearAfter", 20);
forceUppercase = config.ReadBoolean("ForceUppercase", true);
sounds = (Sounds)Enum.Parse(typeof(Sounds), config.Read("Sounds", "Laughter"));
}

This is a common (anti) pattern, but it's easy. Jason made it cleaner:

WPF Configuration Done Better

You should check out Jason's explanation for the deep details. First, he had me use the Settings Tab of the Project Properties. It works with ClickOnce apps, which was why I'd over-engineered the first solution. I assumed it wouldn't work, rather than trying it first. Doh!

image

This designer will generated  Settings.Designer.cs file that is a basic class that fronts your storage. It's from here that we'll get/set our configuration settings and it'll appear in the BabySmash.Properties namespace.

Previously, my Options Dialog was full of what I call "left-hand/right-hand" code. This is the kind of code you go a=b and then b=a, moving data back and forth between "hands." My original code is monkey code. Boring, dull, repetitive and error-prone.

How does he make it cleaner? We use Data Binding in the XAML for our dialog. I've bolded the interesting bits.

<Window x:Class="BabySmash.Options"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="clr-namespace:BabySmash.Properties"
xmlns:l="clr-namespace:BabySmash"
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}">
...Snip...
<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,7,0"/>
<Label Height="23" Grid.Row="1" Margin="10">Sounds</Label>
<ComboBox
SelectedValue="{Binding Path=Default.Sounds}"
SelectedValuePath="Content"
Grid.Column="1" Grid.Row="1" Height="23" Margin="15,0,7,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="OK_Click" >OK</Button>
<Button IsCancel="True" Margin="5,7,7,7" Padding="15,0,15,0" Click="Cancel_Click" >Cancel</Button>
</StackPanel>
</Grid>
</Window>

This chunk of XAML took me a while to get, then it clicked. First, you have to associate an XML namespace with a CLR namepace. That's this line that associates "local:" with the Properties namespace from the Settings class that was created earlier by the properties dialog.

xmlns:local="clr-namespace:BabySmash.Properties"

Next, we tell the Grid that's laying out the controls that we have some static data we'll be using and we call it "settings" because that's the class name.

<Grid DataContext="{StaticResource settings}">

Ok, make sense so far. Remember the dialog looks like this:

image

Removing the layout code, the data binding for these three controls looks like this. The Settings object has a Default Property with a ClearAfter property, and there's also Sounds and ForceUppercase properties.

<TextBox Text="{Binding Path=Default.ClearAfter}"/>
<ComboBox SelectedValue="{Binding Path=Default.Sounds}" SelectedValuePath="Content">
    <ComboBoxItem>None</ComboBoxItem>
    <ComboBoxItem>Laughter</ComboBoxItem>
    <ComboBoxItem>Speech</ComboBoxItem>
</ComboBox>
<CheckBox IsChecked="{Binding Path=Default.ForceUppercase,Mode=TwoWay}">Force Uppercase</CheckBox>

The one that Jason didn't do that flummoxed me for a bit was the ComboBox. The settings were being saved, but the dialog loaded, the loaded value wasn't selected in the ComboBox. I assume that the SelectedValue would be the text in each item. Then I realized that as Combos can have Content, Tags, and other bits of data hanging on them, the system needed to know the "Path" to a value to use when setting SelectedValue. The text is stored in ComboBoxItem.Content, so I set SelectedValuePath.Content.

For an alternative view this is how it would be done if I was using Tags and storing these numbers rather than strings in my config:

<ComboBox SelectedValue="{Binding Path=Default.Sounds}" SelectedValuePath="Tag">
    <ComboBoxItem Tag="0">None</ComboBoxItem>
    <ComboBoxItem Tag="1">Laughter</ComboBoxItem>
    <ComboBoxItem Tag="2">Speech</ComboBoxItem>
</ComboBox>

At this point, ALL the code in the Options.xaml.cs goes away except a call to Save. There's no left-hand/right-hand code, just markup that is associating the controls with the data they should be bound to. It's a subtractive refactoring and the whole thing gets cleaner, simpler, and DRYer (Don't Repeat Yourself). All the local variables in the main window also go away, as I can just access the property for Settings.Default.ForceUppercase or whatever and it's all centralized.

private void OK_Click(object sender, RoutedEventArgs e)
{
Properties.Settings.Default.Save();
this.Close();
}

Coding via subtraction/deletion is my favorite kind of coding. Here's how Jason explained the whole process:

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.

Related Links

Technorati Tags: ,

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.

facebook twitter subscribe
About   Newsletter
Hosting By
Hosted in an Azure App Service
June 03, 2008 11:25
When you add settings via the project's settings tab, where do the settings get stored? In what format? Does it only work with ClickOnce? For some reason I thought that tab set stuff in the app.config, but then you couldn't write to it at runtime since app.config is in Program Files.
June 03, 2008 11:51
They'll get stored in a user.config in a folder like this:

C:\Users\scottha\AppData\Local\Microsoft\BabySmash.exe_StrongName_jukdg213ihkqtjzhinyp0obr4fkte2mb\1.0.0.0
June 03, 2008 17:02
This is interesting Scott, I too put together a very unwieldy first attempt WPF program and will follow this guide with interest to best practices.

Great idea.
June 03, 2008 17:07
What about transactional support? If I hit cancel I want to revert back to the old settings.
Also, isn't it a better design to make the dialog generically bind to any instance of the settings class and set the dataContext from the calling form? Probably doesn't matter here, but generally I think it reduces dependencies.
Joe
June 03, 2008 18:31
Hey, you need one more line of code, in the Cancel_Click handler:

Properties.Settings.Default.Reload();

Otherwise the changes you make in the config dialog will hang around until you restart the app.
June 03, 2008 18:46
Another thing...

The one big paradigm shift I experienced when moving to WPF is realizing that if you front units of your business logic with DependencyObject "controllers" you can pretty much remove all codebehind in your UI. For instance imagine this common scenario:

You want to display a list of products in a category, you have a category dropdown and a products list in the UI. In your business logic, you have a method that returns a list of categories and a method that returns a dataset of products based on a category name.

Normally, you'd have to add codebehind for the form load event to populate the dropdown from the business logic, then on the slelectedindexchanged event of the dropdown etc blah blah yadda yadda.

But in WPF, you can create a DependencyObject "controller" that exposes a dependency property for the category list and the product list, implemented as observable lists. Inside this controller, you populate the category list when instantiated. Then you listen for the selected value of the category to change, at which point you populate the products list. All you have to do at that point is to databind against these properties in the UI. Its a nice pattern, and pretty easy to unit test both the UI and the controller.


June 03, 2008 19:08
Given that your tags match up exactly with the indices of the ComboBoxItems, is there any reason that you didn't just bind to SelectedIndex? This would allow you to drop both the tags and the SelectedValuePath.

Also, it's more of a style/idiom question that has yet to be answered, but at the company where I work, the Grid on which you directly set the DataContext would probably be refactored into a DataTemplate for {x:Type local:Settings}, and the instance of local:Settings in your resources would become the single content element of the window itself.

In the case of a settings dialog, it's not a huge deal, but if this were some sort of class that you'd be showing in several different places, it would allow you to save quite a bit of copy/pasting :)
June 03, 2008 19:27
Scott,

Thanks for doing this series. The way you explain things is very simple and easy to understand. As someone who comes from a Win32/WinForms background, I'm in the same boat as you.

Thanks again.
June 03, 2008 21:08
David - I'm not sure I understand the second point? I get the first one. I should change the setting from a string to an int and store the tag rather than the string. I don't understand the DataTemplate....

Will - good catch!

Thanks!
June 03, 2008 21:29
Will - your point about the DependencyObject controller -
Agreed, but why a DependencyObject? We do the same thing but using ObservableCollections and POCs supporting INotifyPropertyChanged.
In fact we heavily rely on CLINQ (see CodePlex) to do what you suggest, but using dynamic resultsets generated from LINQ queries.
Joe
June 03, 2008 22:16
I've made some improvements to the improvements which can be found on my blog at http://icr.ac.webfusion.co.uk/post/Improving-the-Improvements---Cleaning-up-and-adding-validation-to-Baby-Smash!-Options.aspx
June 03, 2008 22:42
Scott - Imagine a world where any POCO could actually be represented as a control in WPF. That's what DataTemplate does for you.

To use a DataTemplate in this way, create a DataTemplate element in your Window/Application/whatever's Resources, and set the DataType to whatever type the template is for (don't specify an x:Key attribute).

The DataTemplate can have a single child, which should be some sort of control/gui object (like a Grid), and all descendants of the DataTemplate will implicitly have their DataContext set to whichever object is being templated.

Once you've done this, you can drop instances of your templated type anywhere you could normally have a control, and WPF will automagically find and apply the template.

Another way to use DataTemplates is to specify a key on them (at which point the DataType attribute becomes optional but often recommended) and then specify that resource on various properties that accept datatemplates, like the ItemTemplate property on ComboBox or the ContentTemplate property on ContentPresenter (you can use ContentPresenter in places where you'd like to refer to an existing object instead of creating a new one--for example, you could leave your settings object in the resources, and have a ContentPresenter with its Content set to that object).

Obviously, I'm simplifying a lot of things, but that's how it works, in a nutshell. Does that help at all?
June 04, 2008 0:12
Joe -
(spoiler: I'm not a WPF expert! I'm just IMHOing)

If you can get away with it, you can definitely bind against POCO's with properties that expose ObservableCollections. But there are a couple benefits to extending from a DependencyObject that I've noted. First, you can expose non-collection properties and bind against them without any OneWayToSource nonsense. Second, you can override the OnPropertyChanged method of the DO in order to execute code.

For example, let's say you have a SearchManager DO that has a SearchFor dependency property of type string and a PossibleMatches observablecollection of type whatever. You want to bind the SearchFor property to a textbox and the PossibleMatches property to a custom control that acts like a tooltip with a listbox that shows possible search values for the textbox. When somebody types text into the search textbox (you'd have to change the event that fires the update on the textbox to do this, btw), your dependencyproperty will change. The OnPropertyChanged event fires, you examine the event args to see if the property that changed is the SearchFor textbox, and if it is, you perform your search of possible searches(?) and populate the PossibleMatches collection. This in turn changes what you display in your custom control automatically. No codebehind, just pure binding sweetness and light.

I don't think this is possible with a POP (plain old property?). You have to use a dependency property in this case.

June 04, 2008 1:12
Scott -

Just another fanboy rave for this series. The first post got me hooked with the idea, but this one gives me a very clear picture from how my mental picture of the app might work to the 'WPF way' of doing it. It's that kind of thing that I think is essential to really grasp big architecture shifts like this. Thanks for the good work and I'm hungry for more!
June 04, 2008 2:26
Will - there's another alternative to inheriting from DependencyObject to get all of the wonderful binding goodness: you can also implement INotifyPropertyChanged. DependencyObjects are rather heavy, in the grand scheme of things, so we tend towards implementing INotifyPropertyChanged wherever possible (we also have our own lightweight "default" implementation of the interface that we quite commonly inherit from).

Also, it's worth keeping in mind that for DependencyObject and INotifyPropertyChanged instances that are consumed by WPF, you're implicitly committing yourself to a certain level of thread affinity--if you raise PropertyChanged from a non-UI thread, be prepared for trouble.
June 04, 2008 5:33
Scott, Why did you have to mention google bandwidth in a recent pod cast. I agreed with you until the last 2 days. You seem to have put the mocker on them. I have had trouble connecting to gmail web interface, at first I thought it must be my connection but other sites worked fine at the same time as gmail problem. So I thought it was machine but nop other machine same issue. Then as i was testing bang working again. Both times now seems to be a very short outage but it still is there. Unlike IMAP connection to gmail which has failed constantly but I can live with that as the bandwidth here must be huge.

Of course one thing you did not mention in your podcast when you were talking about updates of sites etc. Living here in Australia we see it often as we are online when you guys are asleep or updating. Over the years I have visited some big name sites and found them down, albeit politely down. And some IM servers too. I remember a few years back one IM provider was down every Monday morning you could not authenticate on Monday mornings week after week. Also allot of us here use hosts in the USA due to much lower costs however if there is a planned outage at that hoster they always plan it at a good USA time which is often the worst Australian time. To all USA providers a Sunday night USA time outage is the worst time for Australia as that is Monday morning here.




June 04, 2008 7:56
Scott,

Funny enough I decided to build config file support for our media center videobrowser project yesterday.

I had a very different set of requirements and ended up rolling my own which in my opinion is awesome. Here are some of the reasons I did this: (and Im not sure baby smash has the same requirements)

1. I wanted my config file to live in the c:\programdata\videobrowser path which makes more sense for our project
2. I wanted to control persistence
3. I wanted to automatically recover from users deleting, editing and mangling the file, gracefully (by reverting to defaults)
4. I wanted pretty comments in my config file.
5. I wanted this to be super easy for developers to consume

So I wrote a little Config class.

From the developer perspective to add a setting, all that is required is adding a couple of lines to the config file, for example:


[Comment(
@"A lower case comma delimited list of types the extender supports natively. Example: .dvr-ms,.wmv")
]
public string ExtenderNativeTypes = ".dvr-ms,.wmv";


To use the setting you call

Config.Instance.ExtenderNativeTypes;


From the users perspective the config file looks pretty and easy to edit:

<!--A lower case comma delimited list of types the extender supports natively. Example: .dvr-ms,.wmv-->
<ExtenderNativeTypes>.dvr-ms,.wmv</ExtenderNativeTypes>


Feel free to check out the source, its out there on google code.

Cheers
Sam




Sam
June 04, 2008 17:38
Old habits die hard! When you find a programming technique which works, there's a little voice inside you crying, "The new way won't work!" or "Why reinvent the wheel?" when you first make the change.
June 07, 2008 6:55
Sam makes some very good points about why the built in .NET config stuff just doesn't work very well for doing commercial apps.

Scott, you said it yourself. Try explaining to the IT guy at your first big customer why your config is in:

C:\Users\scottha\AppData\Local\Microsoft\BabySmash.exe_StrongName_jukdg213ihkqtjzhinyp0obr4fkte2mb\1.0.0.0

Using the "that's just the way MS does it" line, likely won't cut it.

Sam's little config class is short and sweet. I'll be digging into it a little deeper, I suspect.

Thanks to both of you for writing!

Comments are closed.

Disclaimer: The opinions expressed herein are my own personal opinions and do not represent my employer's view in any way.