Learning WPF with BabySmash - Manually Managing ClickOnce and some more Designer Goodness
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."
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.
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:
And here's the Felix version:
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!
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.
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:
- Smaller Client Profile for machines without .NET on them. From like 75megs down to like 25megs with a 200k bookstrapper.
- Firefox Support for ClickOnce.
- Faster startup.
- More hardware accelerated effects in WPF.
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.
About Newsletter
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.
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!
Can't you do the same for BabySmash, ie. just update the darn thing when the user closes the application?
See insecure 3rd party software updaters (found via Michael Howard).
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?
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.
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
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));
}
foos.Where(f => !goos.Any(p => f.Name == p.Name)))
I didn't actually test this, but it looks right to my memory
I like the new options dialog, but it seems to have triggered a bugbear - there is no "cancel" button.
http://code.google.com/p/wittytwitter/source/browse/#svn/trunk/Witty/Witty/ClickOnce
If anyone has any questions on it, just contact me.
-Keith
- Oli
(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.
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).