Scott Hanselman

Learning WPF with BabySmash - Manually Managing ClickOnce and some more Designer Goodness

August 01, 2008 Comment on this post [22] 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 Feedback and we'll all learn WPF together. Pick a single line, a section, or subsystem or the whole app!

One of the pieces of feedback on BabySmash! was that even though people like that the application automatically updates (via ClickOnce) when they sit down to play BabySmash! their babies want to play it NOW. They didn't like that it tries to update itself when you launch it.

When you setup a ClickOnce application, you get a few choices. You can have the application check for updates before it starts and applying updates before it starts, or you can have it check after it starts and install the updates the next time it starts up. Or, you can say Don't Check For Updates.

I personally find this dialog a little confusing. What this really means is "I'll check manually in code."

Application Updates

At this point, I'm still deploying my app as a ClickOnce app, but now the actual updating is up to me. I used to pop up the Options dialog every time the app started, but again, folks complained, so I had to figure out a way to let them know that an update is available without "stopping the action."

When the app starts up now, it fires of a call to CheckForUpdateAsync from inside System.Deployment.Application and listens for a response. This happens in the background (hence "async"):

if (ApplicationDeployment.IsNetworkDeployed)
{
ApplicationDeployment deployment = ApplicationDeployment.CurrentDeployment;
deployment.CheckForUpdateCompleted += deployment_CheckForUpdateCompleted;
try
{
deployment.CheckForUpdateAsync();
}
catch (InvalidOperationException e)
{
Debug.WriteLine(e.ToString());
}
}

If there is an update, I show a label in each MainWindow:

void deployment_CheckForUpdateCompleted(object sender, CheckForUpdateCompletedEventArgs e)
{
ClickOnceUpdateAvailable = e.UpdateAvailable;
if (ClickOnceUpdateAvailable)
{
foreach (MainWindow m in this.windows)
{
m.UpdateAvailableLabel.Visibility = Visibility.Visible;
}
}
}

The label is really simple, just a label with a glow:

<TextBlock x:Name="UpdateAvailableLabel" Visibility="Collapsed" Margin="15,0,0,0" FontSize="12">
<TextBlock.BitmapEffect>
<BitmapEffectGroup>
<OuterGlowBitmapEffect x:Name="UpdateGlow" GlowColor="Red" GlowSize="3"/>
</BitmapEffectGroup>
</TextBlock.BitmapEffect>
<Bold>Update Available - Visit Options to Update BabySmash!</Bold>
</TextBlock>

Except the glow "pulses" between three colors and repeats forever. I set the DesiredFrameRate to a low number like 10 fps rather than what WPF will attempt, which is 60 fps! I'll post later how we can detect how awesome the user's hardware is and scale or turn off animations and effects.

<Window.Resources>
<Storyboard x:Key="Timeline1" Timeline.DesiredFrameRate="10">
<ColorAnimationUsingKeyFrames BeginTime="00:00:00" RepeatBehavior="Forever"
AutoReverse="True" Storyboard.TargetName="UpdateGlow"
Storyboard.TargetProperty="GlowColor" >
<SplineColorKeyFrame Value="#FFCDCDCD" KeyTime="00:00:00"/>
<SplineColorKeyFrame Value="#FFB92121" KeyTime="00:00:01"/>
<SplineColorKeyFrame Value="#FF2921B9" KeyTime="00:00:02"/>
</ColorAnimationUsingKeyFrames>
</Storyboard>
</Window.Resources>

<Window.Triggers>
<EventTrigger RoutedEvent="FrameworkElement.Loaded">
<BeginStoryboard Storyboard="{StaticResource Timeline1}"/>
</EventTrigger>
<EventTrigger RoutedEvent="FrameworkElement.Loaded"/>
</Window.Triggers>

Now I've got a nice passive FYI that there's an update.

image

I want to show an update button when the user visits the Options Dialog, which brings me to my awesome volunteer designer Felix Corke (blog). If you want a great WPF or XAML designer, give Felix money. Here's my original Option Dialog:

OldBabySmashOptions

And here's the Felix version:

image

Seriously. It hurts. Brings a tear to my eye.

When the user needs to update, I'll do two things. First, there will be a button that says UPDATE!

updatingoptions

Second, after they've hit Update there will be a Progress Bar and that will update as the new version is download in the background.

 optionsdialogupdating

The API is surprisingly easy to use. We check to see if we were launched from the Network, then I check again (really not needed since I did it earlier, but I like to double-check) for an update. This isn't asynchronous, but it's fast.

I setup two event handlers to listen to the UpdateProgress changing, and to get notification when the system has completed the download of the update. Then I fire off an asynchronous update. 

private void updateButton_Click(object sender, RoutedEventArgs e)
{
if (ApplicationDeployment.IsNetworkDeployed)
{
ApplicationDeployment deployment = ApplicationDeployment.CurrentDeployment;
if (deployment.CheckForUpdate())
{
MessageBoxResult res = MessageBox.Show("A new version of the application is available,
do you want to update? This will likely take a few minutes...",
"BabySmash Updater", MessageBoxButton.YesNo);
if (res == MessageBoxResult.Yes)
{
try
{
deployment.UpdateProgressChanged += deployment_UpdateProgressChanged;
deployment.UpdateCompleted += deployment_UpdateCompleted;
deployment.UpdateAsync();
}
catch (Exception)
{
MessageBox.Show("Sorry, but an error has occurred while updating.
Please try again or contact us a http://feedback.babysmash.com. We're still learning!",
"BabySmash Updater", MessageBoxButton.OK);
}
}
}
else
{
MessageBox.Show("No updates available.", "BabySmash Updater");
}
}
else
{
MessageBox.Show("Updates not allowed unless you are launched through ClickOnce from http://www.babysmash.com!");
}
}

The ProgressChanged event is really convenient because it includes the percentage complete! One less thing for me to do.

void deployment_UpdateProgressChanged(object sender, DeploymentProgressChangedEventArgs e)
{
this.updateProgress.Value = e.ProgressPercentage;
}

The Completed event is also fairly tidy (this one is simplified, and you could always use extra error handling. Note that since this was an asynchronous call, any exceptions that might have occurred elsewhere will show up here in the Error property of the AsyncCompletedEventArgs parameter.

void deployment_UpdateCompleted(object sender, System.ComponentModel.AsyncCompletedEventArgs e)
{
MessageBoxResult res2 = MessageBoxResult.None;
if (e.Error == null)
{
res2 = MessageBox.Show("Update complete, do you want to restart the application to apply the update?",
"Application Updater", MessageBoxButton.YesNo);
}
else
{
MessageBox.Show("Sorry, but an error has occured while updating. Please try again or contact us a http://feedback.babysmash.com. We're still learning!",
"Application Updater", MessageBoxButton.OK);
}
if (res2 == MessageBoxResult.Yes)
{
System.Windows.Forms.Application.Restart();
}
}

I'll update this app to .NET 3.5 SP1 when it ships and I'll get a bunch of new features to make BabySmash! better like:

All in all, not much code for me to switch from an automatic ClickOnce Deployment that I had no control over to one I now have not only complete control over, but also one that fits in more nicely with our always improving UI. Also, an FYI, ClickOnce works with WinForms or WPF equally.

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
August 01, 2008 10:20
Awesome timing! I have to do some ClickOnce updates for our apps and never dove into doing manual updates. I like some of the stuff done here (although the business apps are not using WPF yet).

I do need to ping a ClickOnce person though. If I setup a pre-requisite for .NET 3.0 using Visual Studio 2005, I can't publish the ClickOnce app. Even doing manual setup (via ClickOnce in a .msbuild project) it rejects it. Not sure if it's broken or not.

Enough of my babbling and rants. Nice post, love the new options screen (us developers/programmers can't build UIs if our lives depended on it).
Bil
August 01, 2008 10:23
Weird...my ClickOnce experience has all been under 3.5, and I would really recommend using 3.5SP1 as it has a lot of ClickOnce goodness in it.
August 01, 2008 10:25
It seems that you have stumbled upon the greatest UX design point. If a 2 year old can do it surely anyone can do it... I may write a book on it! Seriously though:

One of the pieces of feedback on BabySmash! was that even though people like that the application automatically updates (via ClickOnce) when they sit down to play BabySmash! their babies want to play it NOW.

People want things to work in their favour. Things should happen automatically. The user shouldn't have to think about what they are doing!

All in all...its interesting to watch what you learn by making this project open source. Regardless of everything else...you will learn how to make a proper form for user interaction. It doesn't matter what it does or who its for...it will be designed so the user can use it as simply as possible.

It's unfortunate in this case...as it should take more than making a project open source to make it easy to use.
August 01, 2008 10:29
Interestingly enough, though, people NOW have to think about updating the application by going to the options screen. I wonder if I should force an update at some point?
August 01, 2008 10:30
Scott,

This is very interesting stuff. I've been wondering how to deal with ClickOnce updates via code and this did the trick. Also, the new options window IS beautiful.

Small Bug Report: When you bring up the options dialogue, the dialogue does not have focus, therefore you can't use the mouse to make selections...at least on my Vista box here. I had to tap the Alt key in order to make selections in the options dialogue window.

Anyway, this is coming along nicely!
August 01, 2008 10:39
It's amazing how much better designers really are at designing. *sigh*
August 01, 2008 12:50
Do you need to bother the user with the update? Seriously, I love the Windows Update notifications; Your machine has been updated, click here to see what/why. Excellent! :) No need to bother me with it; I don't care! I trust Microsoft to know what's best for Windows, I sure as heck don't know... ;)
Can't you do the same for BabySmash, ie. just update the darn thing when the user closes the application?
August 01, 2008 14:56
Are you checking that the downloaded updates are genuine? Looks like malware is more starting to target auto-updaters.

See insecure 3rd party software updaters (found via Michael Howard).
August 01, 2008 18:22
I was talking to a potential client who mentions they want someone with Silverlight experience to provide an alternative to their thin client solutions. They never mentioned WPF. After trying to convert my WPF app to Silverlight so I could speak with confidence about creating LOB apps w/Silverlight, I had little success. By the time I deleted all the mechanisms in WPF that were not in Silverlight, I had an absolute mess; less than a shadow of it's former self. The styles/templates available via eg. www.xamltemplates.net don't work in Silverlight. Flash/Flex is to me btw non-trivial. With FlexBuilder/WebORB, you can run but you can't hide from the complexity. Using VS08 for the Intellisense and Blend for moving around the visual components like Tinkertoys, the experience is already promising, never mind future expectations for these products.
I had good success with ClickOnce a year ago with one client in a Winforms/VS05 internal network/vpn context. Will WPF via an optimized Clickonce work?....for companies who are trying to satisfy their clients insistence on a thin client but who really do want things WPF is offering (and the things we as solutions providers know we can provide via enhancements in a 'timely' fashion?) Are we at that point where the cost/benefit takes us to that conversation where can recommend to our end user's who require more than standard data entry screens ( ie. dashboards of some complexity) that the initial inconvience of the .NET 3.5 runtime download onto their XP boxes or MAC's using Vmware's Fusion or bootcamp, is worth it? Is security the issue? Am I the only one not doing megaTon downloads for other things? I don't like to admit that Linux users will have to await a Moonlight solution. But once that download is done and the app/UI is already on their local machine, they will only be moving data via service endpoints and http, with no postback/viewstate/javascript/etc. conundrum. As I recall, the clickonce workflow downloaded the necessary .NET runtime (with a prompt to bail out if desired) for the user without much fuss. Am I grinding the wrong coffee beans? Do I have to play in the sandbox without all my toys? CFLs will eventually give way to LEDs but why not use something that works in the interim?
August 01, 2008 19:31
Richard - Thanks!

Greg - Lot of questions there. ;) The problem I think you ran into was starting with WPF and "down-porting" to Silverlight. I can see how that would be a mess. Because WPF is a superset of Silverlight's XAML, you'd need to start in Silverlight and port up if you wanted to get something that worked in both places.

The .NET 3.5 download will drop to around 25 megs with the new Client Profile runtime and become a lot less inconvenient.

You should talk to Tim Heuer about Silverlight LOB applications.
August 01, 2008 20:02
Neat stuff! I would recommend changing the Options dialog updating feature as follows:
1 - If the async update check at startup didn't say you had an update available, the button should read "Check for update" and then the "Update found, download and install it?" MessageBox is justified when you do find an update (although, it would be even better if it behaved like change #2 below). If not, the button can be disabled and renamed to "No updates available" until the next time the Options dialog is opened OR 1-2 minutes have elapsed.

2 - If the async update check at startup said you had an update available, then the button can read "Update BabySmash!" and you can skip the MessageBox when the user clicks it.

3 - After (if?) the update is successful, the button can then be renamed to "Restart BabySmash! to complete the update", thus preventing yet another MessageBox. (imagine the download takes a while and your baby wants to continue playing in the meantime) You can re-use the unobtrusive "Update available" flashy FYI thingy to display "Restart BabySmash! to complete the update" when the time comes.

4 - Similarly for "Sorry, error!" message boxes that can be replaced with "flash FYI thingy".

Message boxes are evil. Your baby should get used to never having to deal with them. :)

Cheers,
- Oli
August 01, 2008 20:11
Oli - Those are EXCELLENT ideas. Thanks! You want to implement them? The source is at http://www.codeplex.com/babysmash
August 01, 2008 22:09
Since Baby Smash is all about writing code better, I have a question about writing something in an even more .net 3.5 way. I tend to be writing this type of code a lot


FooCollection foos = FooCollection.GetAllFoos();
GooCollection goos = GooCollection.GetAllGoos();

foreach(Foo foo in foos.Where(f => goos.FirstOrDefault(p => f.Name = p.Name) == null)){
//Do something like create a matching Goo
goos.Add(new Goo(f.Name));
}




Any suggestions on making the foos.Where(f => goos.FirstOrDefault(p => f.Name = p.Name) == null)) more readable? I have created my own DoesNotContain() extension method, but I still think it can be improved on:

FooCollection foos = FooCollection.GetAllFoos();
GooCollection goos = GooCollection.GetAllGoos();

foreach(Foo foo in foos.Where(f => goos.DoesNotContain(p => f.Name = p.Name))){
//Do something like create a matching Goo
goos.Add(new Goo(f.Name));
}


August 01, 2008 23:04
In my experience, a "passive" auto-update results in users who never update. I tried it in Paint.NET! Users just did not notice the little reminder/notification thing. The other two ways to do it are the in-your-face approach (which Paint.NET currently employs), or the way Firefox does it where they do the update once you exit the current session.
August 02, 2008 0:51
Am I the only one to notice the clipped labels in the new options?
Stu
August 02, 2008 2:30
Eek! Well, Oli just gave me a patch for those issues, Stu, and I'm adding them now.
August 02, 2008 2:31
Daryl, maybe this is easier for you to read

foos.Where(f => !goos.Any(p => f.Name == p.Name)))

I didn't actually test this, but it looks right to my memory
August 02, 2008 6:12
Hi Scott,

I like the new options dialog, but it seems to have triggered a bugbear - there is no "cancel" button.
August 02, 2008 18:02
I created a ClickOnce WPF update library that we use in Witty Twitter (another WPF clickonce deployed app). This one is a little different as it checks at a specific internal based on the config settings. Thus a user doesn't have to click on an update button, it just checks in the background at a specified interval and alerts the user if there is an update. Of course prompting a baby to update their app would confuse said baby thus this isn't anything BabySmash should worry about but may help someone out nonetheless. Here's is the link to the source code:

http://code.google.com/p/wittytwitter/source/browse/#svn/trunk/Witty/Witty/ClickOnce

If anyone has any questions on it, just contact me.

-Keith

August 03, 2008 0:11
The "Cancel button" has been "restored" with the same patch that fixed the dialog width; just hit the "escape" key on your keyboard, as soon as Scott releases that version, that is. :)

- Oli
Oli
August 03, 2008 7:45
It's amazing how much better designers really are at designing.
August 07, 2008 16:16
Daryl, your code is doing 3 things - defining the domain you're going to search over, defining the query and iterating over the results. Currently, defining the query domain is one step and defining the query and iterating over the results are mashed together in another step. Things can be made to feel a little neater by swapping that over - define the query domain within the query, and then iterate over the results.

(uhpl1 has already given a nice query, so we'll pinch that)


var foos = from f in FooCollection.GetAllFoos()
let goos = GooCollection.GetAllGoos()
where !goos.Any(p => f.Name == p.Name)
select f;

foreach(Foo f in foos) { /* do */ }


I'm a big fan of keeping the query and the iteration separate, and think this makes things a lot neater. What do you reckon?

Cheers
Matt

Comments are closed.

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