Justin Toth's Blog

Justin is a web developer living in Maryland

Manually programming automatic updates into your .NET WPF app

clock February 6, 2011 01:25 by author Justin Toth

I have a WPF app that I created a few weeks ago. For starters, I created an installer using Click Once Deployment. COD's best feature is that it handles updates of your app automatically. You can publish a new version of your app to a web server, and then when users open the app, it'll see that there's a new version and download/install it. However, COD seems to be a half-baked solution and has a number of shortcomings. One example is that there is no good way to add your app to startup on windows boot.

The natural option is to switch over to using a .NET setup project, which generates a .MSI installer for your app. The setup project is a solid solution and allows all of the basic features such as adding your app to startup, start menu, desktop, etc... However, one glaring thing missing is the ability to handle automatic updates, so in this regard it falls short of COD. There are valid reasons for why it doesn't have this ability, which I won't go into in this post, but we still need a way to handle automatic updates in a good way.

The recommended approach is to upload the latest setup (.MSI) file to a web server and then on application startup, you can follow these steps:

 

  1. Check if a new version of the application is available at the web server url.
  2. If a newer version is available, download it.
  3. Generate a .cmd file that will install the new version over the old one.
  4. Run the .cmd file.
  5. Close the existing application.
Here are more detailed instructions and some code so that others don't have to reinvent the wheel.

First, on a web server somewhere, copy the latest .MSI generated by the setup project to that location. Also create an xml file called Version.xml at that location:


<?xml version="1.0" encoding="UTF-8" ?>
<root>
  <versions>
    <version>
      <name>Housters Crawler 1.06</name>
      <number>6</number>
      <url>http://tothsolutions.com/apps/housterscrawler/HoustersCrawler.msi</url>
      <date>2/5/2011 9:00 PM</date>
    </version>
  </versions>
</root>

Next, create a new class in your WPF project called VersionHelper.cs. Also make sure to add a "Version" app key in your app.config appSettings section, which you will increment with each new version and should match the version/number in the xml file on the server.

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Configuration;
using System.IO;
using System.Xml;
using System.Xml.Linq;
using System.Net;
using System.Diagnostics;
using System.Windows;
using System.Reflection;
using System.Runtime.InteropServices;
using System.Management;
using System.Threading;

namespace Housters.Tray.Installer
{
    public class VersionHelper
    {
        private string MSIFilePath = Path.Combine(Environment.CurrentDirectory, "HoustersCrawler.msi");
        private string CmdFilePath = Path.Combine(Environment.CurrentDirectory, "Install.cmd");
        private string MsiUrl = String.Empty;

        public bool CheckForNewVersion()
        {
            MsiUrl = GetNewVersionUrl();
            return MsiUrl.Length > 0;
        }

        public void DownloadNewVersion()
        {
            DownloadNewVersion(MsiUrl);
            CreateCmdFile();
            RunCmdFile();
            ExitApplication();
        }

        private string GetNewVersionUrl()
        {
            var currentVersion = Convert.ToInt32(ConfigurationManager.AppSettings["Version"]);
            //get xml from url.
            var url = ConfigurationManager.AppSettings["VersionUrl"].ToString();
            var builder = new StringBuilder();
            using (var stringWriter = new StringWriter(builder))
            {
                using (var xmlReader = new XmlTextReader(url))
                {
                    var doc = XDocument.Load(xmlReader);
                    //get versions.
                    var versions = from v in doc.Descendants("version")
                                   select new
                                   {
                                       Name = v.Element("name").Value,
                                       Number = Convert.ToInt32(v.Element("number").Value),
                                       URL = v.Element("url").Value,
                                       Date = Convert.ToDateTime(v.Element("date").Value)
                                   };
                    var version = versions.ToList()[0];
                    //check if latest version newer than current version.
                    if (version.Number > currentVersion)
                    {
                        return version.URL;
                    }
                }
            }
            return String.Empty;
        }

        private void DownloadNewVersion(string url)
        {
            //delete existing msi.
            if (File.Exists(MSIFilePath))
            {
                File.Delete(MSIFilePath);
            }
            //download new msi.
            using (var client = new WebClient())
            {
                client.DownloadFile(url, MSIFilePath);
            }
        }

        private void CreateCmdFile()
        {
            //check if file exists.
            if (File.Exists(CmdFilePath))
                File.Delete(CmdFilePath);
            //create new file.
            var fi = new FileInfo(CmdFilePath);
            var fileStream = fi.Create();
            fileStream.Close();
            //write commands to file.
            using (TextWriter writer = new StreamWriter(CmdFilePath))
            {
                writer.WriteLine(@"msiexec /i HoustersCrawler.msi /quiet");
            }
        }

        private void RunCmdFile()
        {//run command file to reinstall app.
            var p = new Process();
            p.StartInfo = new ProcessStartInfo("cmd.exe", "/c Install.cmd");
            p.StartInfo.CreateNoWindow = true;
            p.Start();
            //p.WaitForExit();
        }

        private void ExitApplication()
        {//exit the app.
            Application.Current.Shutdown();
        }
    }
}

Lastly, call the code in your WPF app on start. I commented out the lines where I also show in the system tray that a new version is being downloaded so the user knows.

//check for new version.
                var versionHelper = new VersionHelper();
                if (versionHelper.CheckForNewVersion())
                {
                    //NotifyIcon.BalloonTipText = "A new version is being downloaded, the crawler will restart once download is complete...";
                    //NotifyIcon.ShowBalloonTip(5000);
                    versionHelper.DownloadNewVersion();
                }

We're just about done. There's one catch with what we have done above. If you try to install the downloaded .MSI file over the existing app, it will launch the repair/remove wizard instead of overwriting it. Left click on the setup project in solution explorer and then go to the properties pane. Make sure that RemovePreviousVersion is true and also increment the Version. When it asks if you want to change the Product Code, click Yes to generate a new Guid. Lastly, change the Version in your WPF project's Properties/AssemblyInfo.cs.

Each time you release a new version on the server, you should do the following things:

  1. Increment the AssemblyInfo version in your WPF app.
  2. Increment the setup project version via the properties window.
  3. Increment the "Version" app key in your WPF app.
  4. Build the setup project to generate the new .MSI.
  5. Copy the new .MSI to the web server.
  6. Update the version/number in the xml file on the web server.
Now whenever the WPF app is launched, it will check for a new version. If it exists then it will download the new .MSI, generate a .cmd file, run the .cmd file to install the new .MSI over the old one, and exit the app. When the new .MSI is installed then it will launch the app again, assuming that you have set up a custom rule in your setup project for launching the app after install.

You can take it from here, but one tip is that you may not want to only check for a new version on app start. You could easily have some sort of timer that checks every so often for a new version and runs this process async.

 



301 Permanent Redirects in ASP.NET 3.5 & 4.0

clock February 15, 2010 23:23 by author Justin Toth

I've been going through an SEO phase recently and part of that was renaming the aspx files of my ASP.NET 3.5 site to reflect the keywords and phrases i was trying to target. For example, a page called Services.aspx that might target web development services would be renamed Web-Development-Services.aspx. I noticed after renaming the files that I was getting errors because the search engines still had my old page names cached and these pages no longer existed.

To stop the bleeding, I had the customErrors node in my web.config redirect to my home page (this is not a solution!) Next I recreated the old pages (such as Services.aspx) and left them empty. Then in the code-behind of each I added some code:

 

Response.Status = "301 Moved Permanently";

Response.AddHeader("Location", "http://mydomain.com/Web-Development-Services.aspx");

 

What does this do? When someone requests the old outdated url, it sends a 301 permanent redirect to the new url, which lets the search engine know to use the new page.

In .NET 4.0, there will be an even simpler way to do this:

 

Response.RedirectPermanent("~/Web-Development-Services.aspx");

 

Using one of these 301 permanent redirects methods you can easily keep users and search engines up to date on the structure of your site, even if you've renamed all of the pages for SEO purposes! 



Silverlight 2 Navigation

clock May 18, 2009 00:51 by author Justin Toth

I've been working on a Silverlight 2 application for the past week and a half and having started my Silverlight experience with SL3, I took for granted the nice navigation features. With SL2 I was forced to create my own navigation framework, which turned out to be a pain in the butt. I read some solutions on the web but they either weren't nice and clean or they assumed that you wanted to navigate from page to page without having a "master" page. It was at this point that I decided to take a crack at it myself...

The first thing I did was create my Page.xaml, which is my master page. In here I created a StackPanel to hold the usercontrol of the current "page" that the user is on. Alternately instead of dynamically adding usercontrol objects to the StackPanel, you could create a UserControl and set its Content property to the user control of the current page.


 <StackPanel x:Name="spContent" Orientation="Vertical" Margin="10,10,10,10" HorizontalAlignment="Left" />

The next thing I had to do was create a Navigate method in the code-behind of Page.xaml:

 public void Navigate(UIElement page)
        {
            //clear old page.
            spContent.Children.Clear();
            //add new page.
            spContent.Children.Add(page);
        }

Next I created a class called Pages.cs which holds the name and object type of each of our pages. Notice the difference between the HOME_PAGE property and the REGISTER and LOGIN properties. I realized early on that if I declared all of the page properties in the fashion of HOME_PAGE, it would create only one instance and then use that one from then on. The web is stateless but switching between pages in Silverlight is not if you use this method, so if you go to the Register page and fill out the form then go to another page and then back again to Register, you expect the form to be blank again but it won't be. The REGISTER and LOGIN properties will create new instances each time the page is requested to avoid this issue.

public class Pages
    {    
        //public pages.
        public static UserControl HOME_PAGE = new Home();
        public static UserControl REGISTER
        {
            get { return new Register(); }
        }
        public static UserControl LOGIN
        {
            get { return new Login(); }
        }
}

Next I created another class called Navigation.cs, which gives us a nice clean 1-liner way to navigate to a new page. The first thing you'll notice is BasePage, which will be set to the instance of our master page. This gives us a way to call the Navigate method in the master page from here so that in our pages we'll be able to call Navigation.Navigate(myPage); The alternative is referencing the App object, then referencing the master page (App.RootVisual), then calling Navigate from there (ugly!)

We have an overloaded Navigate method that lets us pass in a State object. This is just a way to pass a parameter to the next page you go to. A common use would be if you are on a business object list page and you're going to an edit business object page, you could pass the business object in here and then use it to populate the edit form in the edit page without having to hit the database again (very cool!)

The last thing you'll notice is that we're saving the name of the current page we're on in Isolated Storage, which you can think of as Silverlight's version of a cookie. It gets saved on the end-user's computer and gives us a way to persist the value if they exit our application. An important thing to remember here is that even hitting the browser's refresh will exit our application, which by default would bring them back to our default home page. By saving the page name, we can then bring the user back to the page they were on before they hit refresh.

using System.IO.IsolatedStorage;

public static class Navigation
    {
        //properties.
        public static Page BasePage { get; set; }
        public static object State { get; set; }

        public static void Navigate(UserControl page)
        {
            //erase state.
            State = null;
            //navigate.
            NavigateTo(page);
        }

        public static void Navigate(UserControl page, object state)
        {
            //save state to pass between pages.
            State = state;
            //navigate.
            NavigateTo(page);
        }

        private static void NavigateTo(UserControl page)
        {
            //save current page.
            SavePersistentValue("Current Page", page.ToString());
            //navigate.
            BasePage.Navigate(page);
        }

        #region Isolated Storage

        public static object GetPersistentValue(string key)
        {
            IsolatedStorageSettings appSettings = IsolatedStorageSettings.ApplicationSettings;
            if (appSettings.Contains(key))
            {
                return appSettings[key] as object;
            }
            else
            {
                return null;
            }
        }

        public static void SavePersistentValue(string key, object value)
        {
            IsolatedStorageSettings appSettings = IsolatedStorageSettings.ApplicationSettings;
            if (appSettings.Contains(key))
            {
                appSettings[key] = value;
            }
            else
            {
                appSettings.Add(key, value);
            }
        }

        #endregion

The last step is to handle the Application_Startup event in App.xaml.cs to set our Navigation.BasePage to our master page and then load up the current page we were on before the user exited the application.

private void Application_Startup(object sender, StartupEventArgs e)
        {
            //navigate to home page.
            this.RootVisual = new Page();
            Navigation.BasePage = this.RootVisual as Page;
            //check which page we were on.
            string pageName = Convert.ToString(Navigation.GetPersistentValue("Current Page"));
            if (pageName.Length > 0)
            {//navigate to current page.
                Type pageType = Type.GetType(pageName);
                UserControl currentPage = Activator.CreateInstance(pageType) as UserControl;
                Navigation.Navigate(currentPage);
            }
        }

And that's all folks... If you wanted to switch to the login page you would just do Navigation.Navigate(Pages.LOGIN), nice and clean...



Silverlight 3 ChildWindow Modal Popup

clock May 9, 2009 23:18 by author Justin Toth

Tonight I was looking at the Silverlight Toolkit Samples and found the ChildWindow control. This interested me because the first Silverlight tutorial I ever took was ScottGu's on building an application that can search for Digg articles, found here. When you click on an article in the listbox then a modal popup comes up containing more information. He achieved this modal popup functionality by creating a grey rectangle with some opacity that stretched across the screen horizontally and vertically. On top of that was a rounded border control with the content in there that would show up over the rectangle.

I used the concept from ScottGu's tutorial to create a page on my personal website that has a ListBox of some websites that I've created. When you click on one, the modal popup comes up showing more details of the work I did for that website. The ChildWindow control seemed a bit more polished to me so I decided to give it a go.

The first thing you need to do is make sure your project contains a reference to System.Windows.Controls. Interestingly, my application was a Silverlight Navigation project and already contained references to System.Windows.Controls and System.Windows.Controls.Navigation, which made me realize that the navigation functionality that I had been using must be a part of the Silverlight Toolkit! The next thing you'll want to do is add a control to your project for the modal popup. The cool thing here is that there is already a "Silverlight Child Window" control that you can add:

Here's what I did with my child window control, notice how I don't need to worry about the formatting of the window or the modal functionality, all I need to do is add my content that will go inside the modal popup.


<controls:ChildWindow x:Class="TothSolutions.SL.Controls.WorkPopup"
           xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
           xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
           xmlns:controls="clr-namespace:System.Windows.Controls;assembly=System.Windows.Controls"
           Width="600" Height="300"
           Title="WorkPopup">
    <Grid x:Name="LayoutRoot" Margin="10">
        <Grid.ColumnDefinitions>
            <ColumnDefinition/>
            <ColumnDefinition/>
        </Grid.ColumnDefinitions>
        <Grid.RowDefinitions>
            <RowDefinition Height="Auto" />
            <RowDefinition Height="Auto" />
            <RowDefinition Height="*" />
            <RowDefinition Height="Auto" />
        </Grid.RowDefinitions>
       
        <Image Source="{Binding ThumbNailBig}" Width="250" Margin="0,0,20,0" VerticalAlignment="Center" Grid.Column="0" Grid.Row="0" Grid.RowSpan="3" />
        <TextBlock Text="{Binding Title}" FontSize="20" Foreground="#000" FontWeight="Bold" Grid.Column="1" Grid.Row="0" />
        <HyperlinkButton Content="{Binding HrefLink}" TargetName="_new" NavigateUri="{Binding HrefLink}" FontSize="12" Margin="0,0,0,5" Grid.Column="1" Grid.Row="1" IsTabStop="False" />
        <TextBlock Text="{Binding FullDescription}" FontSize="12" Foreground="#000;" TextWrapping="Wrap" Grid.Column="1" Grid.Row="2" />
        <Button x:Name="CancelButton" Content="Close" Click="CancelButton_Click" Width="75" Height="23" HorizontalAlignment="Center" Margin="0,12,0,0" Grid.Row="3" Grid.Column="0" Grid.ColumnSpan="2" />
    </Grid>
</controls:ChildWindow>

In the code-behind of the ChildWindow I wait until the control is loaded and then set the title of the modal popup based on the DataContext, which we'll pass in from the parent page. The title as well as a close button will show up at the top of the modal popup without us having to do anything more... Note: you could set the title in the constructor if it's a static value, e.g. this.Title = "My super cool modal popup!";

    public partial class WorkPopup : ChildWindow
    {
        public WorkPopup()
        {
            InitializeComponent();
            this.Loaded += new RoutedEventHandler(WorkPopup_Loaded);
        }

        void WorkPopup_Loaded(object sender, RoutedEventArgs e)
        {
            Site site = DataContext as Site;
            this.Title = site.Title;
        }

        private void CancelButton_Click(object sender, RoutedEventArgs e)
        {
            this.DialogResult = false;
        }
    }

Lastly, on the parent page when the user clicks one of the items in my listbox, I create an instance of my ChildWindow, grab the business object for that row, set that business object as the DataContext of my ChildWindow, and lastly show the ChildWindow.

 private void SiteList_SelectionChanged(object sender, SelectionChangedEventArgs e)
        {
            if (SiteList.SelectedItem != null)
            {
                Site site = SiteList.SelectedItem as Site;
                WorkPopup popup = new WorkPopup();
                popup.DataContext = site;
                popup.Show();
            }
        }

And that's it, easy peezie... Happy coding!!



Animating Page Transitions in Silverlight 3

clock May 8, 2009 10:31 by author Justin Toth

I wanted to add simple fade out/fade in animations to my page transitions in my Silverlight 3 app. I decided to tackle the fade in animation first, and my first attempt was to add a FadeIn storyboard to the LayoutRoot Grid of each of my pages like so:


        <Grid.Triggers>
            <EventTrigger RoutedEvent="Grid.Loaded">
                <BeginStoryboard>
                    <Storyboard x:Name="FadeIn">
                        <DoubleAnimation Storyboard.TargetName="LayoutRoot" Storyboard.TargetProperty="Opacity"
                From="0" To="1" Duration="0:0:0.300"/>
                    </Storyboard>
                </BeginStoryboard>
            </EventTrigger>
        </Grid.Triggers>
 

You also have to add an Opacity of 0 to your LayoutRoot Grid. There are 2 issues with this approach. 1. You can't work in the designer anymore, as it's now set to invisible. 2. There is now duplicate xaml in every page, if you want to make changes to your animations than you'll have to do it in every single page.

My second attempt was to try and fade in/fade out the frame within MainPage.xaml. I set the frame's opacity to 0. The cool thing is that you can add storyboards to be invoked by code rather than attaching them to individual controls:


<UserControl.Resources>
  <Storyboard x:Name="FadeIn">
   <DoubleAnimation Storyboard.TargetName="Frame" Storyboard.TargetProperty="Opacity"
    From="0" To="1" Duration="0:0:0.200"/>
  </Storyboard>
  <Storyboard x:Name="FadeOut">
   <DoubleAnimation Storyboard.TargetName="Frame" Storyboard.TargetProperty="Opacity"
    From="1" To="0" Duration="0:0:0.200"/>
  </Storyboard>
 </UserControl.Resources> 

Next I had some work to do in the code-behind:

        private Button lnkMenu = null;

        public MainPage()
        {
            InitializeComponent();
            this.Loaded += new RoutedEventHandler(MainPage_Loaded);
            this.FadeOut.Completed += new EventHandler(FadeOut_Completed);
        }

        void MainPage_Loaded(object sender, RoutedEventArgs e)
        {
            FadeIn.Begin();
        }

        private void NavButton_Click(object sender, RoutedEventArgs e)
        {
            lnkMenu = sender as Button;
            FadeOut.Begin();
        }

        void FadeOut_Completed(object sender, EventArgs e)
        {
            String page = lnkMenu.Tag.ToString();
            this.Frame.Navigate(new Uri(page, UriKind.Relative));
            FadeIn.Begin();
        }

Now we have all the page transition animation code in one place so we don't have to deal with the maintainability issue of having it in each page. Also the designers for every single page (including MainPage.xaml) will work now that we're not setting the opacity of any of the pages to 0.



About the author

Justin

Justin is a senior developer who has been working with .NET since 2003. His main focus is building highly-interactive web applications using ASP.NET MVC and Dojo or jQuery. Visit his company's site at http://tothsolutions.com.

Page List

Sign in