Desktop Application deployment with auto update (Shimmer/Squirrel) - not working (yet)

Recently, I’m working on a desktop application. Deployment is always a challenge for the desktop application. There is ClickOnce for .Net application, but it doesn’t work perfectly. You can find more issues that are faced by GitHub for WIndows Team. I trust the Github guys and want to give it a try Squirrel for ClickOnce replacement.

The Application

I create a WPF application. To be more real-world-like application, I integrate with awesomium in the application. After I installed Aweominum SDK and add it to my application. I set Copy Local to true to deploy all awesomium binaries to the bin folder. The MainWindow.xaml file will looks like:

<Window
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:awe="http://schemas.awesomium.com/winfx" x:Class="Banshee.MainWindow"
        Title="MainWindow" Height="800" Width="1024">
    <Grid>

    <awe:WebControl 
        HorizontalAlignment="Stretch" 
        VerticalAlignment="Stretch"
        Source="http://jittuu.com"
        />

    </Grid>
</Window>

and, I update AssemblyInfo.cs with

[assembly: AssemblyVersion("1.0.0")]
[assembly: AssemblyFileVersion("1.0.0")]
[assembly: AssemblyInformationalVersion("1.0.0")]

That’s it! This is the only changes I made in File -> New WPF application (.Net Framework 4).

Squirrel (a.k.a Shimmer)

I follow QuickStart from the wiki of Squirrel. But when I reach to Publish Release state, the New-Release cmdlet is not found. After spending a few hours, I notice that Squirrel package is not ready yet (as of 25 April 2014) and there is already pull request about it. So, I just use old version Shimmer by Install-Package Shimmer. It will download all the dependencies and open nuspec file for you.

Now, it is time to publish a release. I invoke New-Release and it ask me to build first. So, I build it and run New-Release again. Still got error! After researching a bit, I realize that it need to Enable Nuget Package Restore. I enable package restore, rebuild and run New-Release again. Now, it works!

It creates Releases folder under solution folder and put all the releases files. I’m lazy person, so I just double click the Setup.exe and install it. It works perfectly. Cool! But, when I check all the deployed binarys in the local appdata, it doesn’t include the awesomium binaries. I uninstall it from Control Panel and check AppData\Local for remaining files. All are cleanly uninstall.

To add all the awesomium binaries, I modify nuspec by adding files:

  <files>
    <file src="bin\$configuration$\Awesomium.*.dll" target="lib\net40\" />
    <file src="bin\$configuration$\avcodec-53.dll" target="lib\net40\" />
    <file src="bin\$configuration$\avformat-53.dll" target="lib\net40\" />
    <file src="bin\$configuration$\avutil-51.dll" target="lib\net40\" />
    <file src="bin\$configuration$\awesomium.dll" target="lib\net40\" />
    <file src="bin\$configuration$\icudt.dll" target="lib\net40\" />
    <file src="bin\$configuration$\libEGL.dll" target="lib\net40\" />
    <file src="bin\$configuration$\libGLESv2.dll" target="lib\net40\" />
    <file src="bin\$configuration$\xinput9_1_0.dll" target="lib\net40\" />
  </files>

I rebuild the solution, publish release and install again. When I check the deployed binaries, I can see all the awesomium binaries as well. So far, so good.

Setup IIS website

Now, it is time to host at IIS. I did: 1. Create a website with port 8080 and point to a folder called RTW. 2. Copy the files from Releases folder (NOT Release under bin) to RTW folder. 3. Download the setup file from something like: http://yourserver:8080/setup.exe and install it.

It is working too! Next, we need to test for update.

Updating itself

Now it is time to add update feature in our application. I hook Loaded event and add the following code to update itself:

private async void Window_Loaded(object sender, RoutedEventArgs e)
{
    var updateManager = new UpdateManager(@"http://yourserver:8080/", "Banshee", FrameworkVersion.Net40);
    using (updateManager)
    {
        var updateInfo = await updateManager.CheckForUpdate().ToTask();
        if (updateInfo == null || !updateInfo.ReleasesToApply.Any())
        {
            return;
        }
        else
        {
            var releases = updateInfo.ReleasesToApply;
            await updateManager.DownloadReleasesAsync(releases, _ => { });
            await updateManager.ApplyReleasesAsync(updateInfo, _ => { });
        }
    }
}
  1. Uninstall the application
  2. Clear the Releases folder
  3. Clear RTW folder at server
  4. Rebuild the solution
  5. Publish new release with New-Release
  6. Copy all the release files to RTW folder
  7. Download and run setup.exe from the server.
  8. Boom! It Crashs!

After struggling a bit, I found that we need to give web access to the releases file in the web server. I add this web config to the web server and then it works.

<?xml version="1.0" encoding="UTF-8"?>
<configuration>
    <system.webServer>
        <staticContent>
            <mimeMap fileExtension="." mimeType="text/plain" />
            <mimeMap fileExtension=".nupkg" mimeType="application/zip" />
        </staticContent>
    </system.webServer>
</configuration>

Releasing new version

For new version, I did:

  1. Update the Title just to make sure it is updated to new version
  2. Increase version to 1.0.1 in AssemblyInfo.cs
  3. Clean the solution
  4. Rebuild the solution
  5. Publish new release with New-Release
  6. Then I got this error. Method invocation failed because [System.Object[]] doesn't contain a method named 'Split'. At C:\Users\soe.moe\documents\visual studio 2013\Projects\Banshee\packages\Shimmer.0.7.4\tools\commands.psm1:77 char:41 + $packages = $releaseOutput.Split <<<< (";") + CategoryInfo : InvalidOperation: (Split:String) [], RuntimeException + FullyQualifiedErrorId : MethodNotFound
  7. I tried to clear Releases folder and *.nupkg in the output folder.
  8. It works after I clear, but I didn’t generate delta package.
  9. If I don’t clear Releases folder, the error is back. :sweat:

Hmm… OK. I’ll break down into a few problems;

  1. If it can generate delta package, it cannot generate setup.exe.
  2. If it can generate setup.exe and full package, it cannot generate delta package.
  3. How about RELEASE file? Above two generate two different RELEASE file.

I’m a bit tired now, let’s call it a day. I’ll find about more and will post again.

ASP.NET MVC Localization - Routing

When we build multi-language web application, we have to think:

  1. where should we store localization token, and
  2. the application flow for first-time users and revisiting-users.

I prefer to store localization token in the URL because it is SEO-friendly and cache-friendly. For examples, http://www.microsoft.com/en-us/default.aspx.

For the first time users - users without stored locale cookie:

  • when he/she navigate to home page (or any page without localization token), we need to redirect to default localized url. For example, we need to redirect from http://www.example.com to http://www.example.com/en-us. or from http://www.example.com/posts to http://www.example.com/en-us/posts. During the redirection, we also need to set the locale cookie for the next visit.
  • when he/she navigate to page with localization token, we need serve the request with provided locale. In the response, we also need to set the locale cookie.

For revisiting users - users with stored locale cookie;

  • when he/she navigate to page without localization token, we need to redirect to localized url based on the user’s cookie.
  • when he/she navigate to page with localization token, we have two scenarios. If the url token and the cookie value are the same, we just need to serve the request. If the url token and the cookie value are different, we take cookie value as higher priority (he/she might click url from somewhere). In that case, we need to redirect to url with localization token - which is from cookie.

Implementation

First, I create a simple HttpHandler to handle redirection.

using System.Diagnostics.CodeAnalysis;
using System.Web;

namespace MvcLocalization
{    
    class RedirectHandler : IHttpHandler
    {
        private string _newUrl;

        [SuppressMessage(category: "Microsoft.Design", checkId: "CA1054:UriParametersShouldNotBeStrings",
            Justification = "We just use string since HttpResponse.Redirect only accept as string parameter.")]
        public RedirectHandler(string newUrl)
        {
            this._newUrl = newUrl;
        }

        public bool IsReusable
        {
            get { return true; }
        }

        public void ProcessRequest(HttpContext context)
        {
            context.Response.Redirect(this._newUrl);
        }
    }
}

I create RouteHandler,a hook to Asp.Net routing, to handle redirection for without localization token in url.

using System.Globalization;
using System.Web;
using System.Web.Mvc;
using System.Web.Routing;

namespace MvcLocalization
{
    public class LocalizationRedirectRouteHandler : IRouteHandler
    {
        public IHttpHandler GetHttpHandler(RequestContext requestContext)
        {
            var routeValues = requestContext.RouteData.Values;

            var cookieLocale = requestContext.HttpContext.Request.Cookies["locale"];
            if (cookieLocale != null)
            {
                    routeValues["culture"] = cookieLocale.Value;
                    return new RedirectHandler(new UrlHelper(requestContext).RouteUrl(routeValues));
            }

            var uiCulture = CultureInfo.CurrentUICulture;
            routeValues["culture"] = uiCulture.Name;
            return new RedirectHandler(new UrlHelper(requestContext).RouteUrl(routeValues));
        }
    }
}

And a wrapper handler of MvcRouteHandler to handle other cases. LocalizedRouteHandler update current thread’s culture information before it delegate to MvcRouteHandler. I believe that the comment in the code should explain well itself.

using System;
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using System.Web;
using System.Web.Mvc;
using System.Web.Routing;

namespace MvcLocalization
{
    public class LocalizedRouteHandler : MvcRouteHandler
    {
        protected override System.Web.IHttpHandler GetHttpHandler(System.Web.Routing.RequestContext requestContext)
        {
            var urlLocale = requestContext.RouteData.Values["culture"] as string;
            var cultureName = urlLocale ?? "";

            var cookieLocale = requestContext.HttpContext.Request.Cookies["locale"];
            if (cookieLocale != null)
            {
                // if request contains locale cookie, we need to put higher priority than url locale
                // user might click the link from somewhere but he/she already set different locale
                if (!cookieLocale.Value.Equals(urlLocale, StringComparison.OrdinalIgnoreCase))
                {
                    // if cookie locale and url cookie are different,
                    // we should redirect with cookie locale
                    var routeValues = requestContext.RouteData.Values;
                    routeValues["culture"] = cookieLocale.Value;
                    return new RedirectHandler(new UrlHelper(requestContext).RouteUrl(routeValues));
                }
                else
                {
                    cultureName = cookieLocale.Value;
                }
            }

            if (cultureName == "")
            {
                return GetDefaultLocaleRedirectHandler(requestContext);
            }

            try
            {
                var culture = CultureInfo.GetCultureInfo(cultureName);
                Thread.CurrentThread.CurrentCulture = culture;
                Thread.CurrentThread.CurrentUICulture = culture;
            }
            catch (CultureNotFoundException)
            {
                // if CultureInfo.GetCultureInfo throws exception
                // we should redirect with default locale
                return GetDefaultLocaleRedirectHandler(requestContext);
            }

            if (cookieLocale == null)
            {
                requestContext.HttpContext.Response.AppendCookie(new HttpCookie("locale", cultureName));
            }
            return base.GetHttpHandler(requestContext);
        }

        private static IHttpHandler GetDefaultLocaleRedirectHandler(RequestContext requestContext)
        {
            var uiCulture = CultureInfo.CurrentUICulture;
            var routeValues = requestContext.RouteData.Values;
            routeValues["culture"] = uiCulture.Name;
            return new RedirectHandler(new UrlHelper(requestContext).RouteUrl(routeValues));
        }
    }
}

Now, we need to hook that route handler to the routing. Just to be neat, I created extension method to RouteCollection.

using System.Web.Mvc;
using System.Web.Routing;

namespace MvcLocalization
{
    public static class RouteCollectionExtensions
    {
        [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Design", "CA1054:UriParametersShouldNotBeStrings",
            Justification = "This is a URL template with special characters, not just a regular valid URL.")]
        public static Route MapRouteToLocalizeRedirect(this RouteCollection routes, string name, string url, object defaults)
        {
            var redirectRoute = new Route(url, new RouteValueDictionary(defaults), new LocalizationRedirectRouteHandler());
            routes.Add(name, redirectRoute);

            return redirectRoute;
        }

        public static Route MapLocalizeRoute(this RouteCollection routes, string name, string url, object defaults)
        {
            return routes.MapLocalizeRoute(name, url, defaults, new { });
        }

        public static Route MapLocalizeRoute(this RouteCollection routes, string name, string url, object defaults, object constraints)
        {            
            var route = new Route(
                url,
                new RouteValueDictionary(defaults),
                new RouteValueDictionary(constraints),
                new LocalizedRouteHandler());

            routes.Add(name, route);

            return route;
        }
    }
}

And added to the routes table when application starts.

using System.Web.Mvc;
using System.Web.Routing;

namespace MvcLocalization.Sample
{
    public class RouteConfig
    {
        public static void RegisterRoutes(RouteCollection routes)
        {
            routes.IgnoreRoute("{resource}.axd/{*pathInfo}");

            routes.MapLocalizeRoute("Default",
                url: "{culture}/{controller}/{action}/{id}",
                defaults: new { controller = "Home", action = "Index", id = UrlParameter.Optional },
                constraints: new { culture = "[a-zA-Z]{2}-[a-zA-Z]{2}" });

            routes.MapRouteToLocalizeRedirect("RedirectToLocalize",
                        url: "{controller}/{action}/{id}",
                        defaults: new { controller = "Home", action = "Index", id = UrlParameter.Optional });
        }
    }
}

That’s it! Now, you have localization-aware mvc application. Of course, I created github repo for the whole source code and sample web application.

User Experience

Every product needs good, if not great, User Experience. When people heard about UX, they usually confuse with User Interface. Acutally, User experince is much bigger topic than User Interface - in fact, user interface is part of the UX. What makes good user experience in software?

First of all, people use software to solve their problem. Every components of UX should have the common goal which is to solve user’s problem. I believe these are the major contributor to User Experience in software.

  • User Interface : The user should know easily what to do and what will they get. Don’t make them think.
  • Workflow : User want to solve their problem fast. Make the steps short. The simpler, the better.
  • System Performance : The faster it is, the better user experience. No one want to use slow system. As jeff said, Performance is a Feature.
  • System Stablility and Reliability : How will user feel if the system crashed after he/she took 30 minutes to fill the form?
  • Uptime : The uptime also contributes a lot to user experience. User expect your system is available 24/7. I believe that every system should have at least 99% uptime - total max downtime will be 3.65 days per year or 7.20 hours per month. It also includes scheduled downtime. (Who care? When it is down, it is not available to use. It doesn’t matter either scheduled or not.).

Coding conventions

Recently, I’m working on creating coding conventions for my company. That is always difficult to create guideline because there is no “right” answer. Everyone has their own preferred style. No matter what coding guidelines we choose, we’re not going to make everyone happy. But I believe that one standard is always better than two standards.

Most of the time, we solve it by creating coding guideline document. The issue with this approach is no one read it. I have worked in a few companies, either big or small, they usually have one coding guideline document (mostly outdated) - but developer don’t read it. They just scan it. Developer are not good at reading documents. Even theywe do read it, but it is impossible to remember all the guidelines while we are coding. Eventually, the guideline document become dead document.

I personally believed in automation. If we can’t automate checking the code against the guideline, I prefer to drop that guideline. Fortunately, there are some tools created by smart people to address the issue. I chose StyleCop and FxCop to address the coding guideline issue for our .NET development. Of course, we don’t bring every rules which are defined in those tools. We picked most of them that is good enough for us. That rules must be living rules which means the rules need to be updated from time to time. Retrospective meeting might be the good place to talk about it.

The workflow will be:

  1. Create team/company-wise setting/rulesets for StyleCop and FxCop
  2. Integrate with the solution and development environment
  3. Integrate with CI (Well, this is one of the good reason to have CI)
  4. Update the setting/ruleset (if required)
  5. Repeat 2-4

I have created coding guideline at HERE. Of course, I would be happy if you can take a look and put some comment on it. :)

How to work hard

Long ago, I used to work late. I worked like 12 hours a day. I thought I was working hard and contributing a lot to the project. It was not only me but everyone was staying late, and it became company culture. Later, we released the project. Since then, people need to do fire-fighting everyday because the software has many bugs. While fixing bugs under pressure, we introduced new bugs - starting bug fixing cycle. Users were not confident to release bug fixes and new features.

I was wrong. I was wrong in thinking that working long hours is working hard. My mind and body is tired after working long hours. I introduced many bugs while working with tired mind. I lied myself to stay late even though I want to leave office. I wanted my boss to think that I was working hard, even though my productivity at that time was near zero.

When I realized that, I think it was 3-4 years ago - I try to change my habit. It helps me a lot when buildingcrafting the software. Recently, I am able to lead two successful agile teams and we are releasing quality software fast. Everyone is leaving the office at 6:00/6:30 PM and yet our development pace is steady fast. What did I change?

How to work hard?

  • Find the zone - We are creating the best software when we are working in the zone.
  • Don’t check email at morning - For many people, including me, the zone is at the morning. My mind is fresh and I still have lots of energy at the morning (of course, if I have relaxed and slept enough last night). Don’t use that moment to check email. Use to craft the software that is what we are paid to.
  • Pomodoro - I find myself that I am very productive while working with pomodoro. I target myself to achieve 4-6 pomodori a day. Yes, there is a max for me. You will surprise that how much energy is required to focus and work productively. I was exhausted after 3-4 pomodori when I start using pomodoro technique.
  • Exercises - Not only our mind but also our body need to be strong. Doing exercises will keep us fresh and help to sleep well at night.
  • Write tests - The best productivity tool I found so far for software development.
  • Leave the office - Know your limit. Our mind and body also need break. I try to leave office at 6:00/6:30 PM. I don’t want to work with tired mind just to create more bugs. I don’t want to waste my zone at tomorrow morning.
  • Do side project - start doing any hobby project or may be start to participate in some open-source project that you are interested. It also benefits to the company because: your skill will be increased, you may already solved some problems in your project which may encounter in your work, you may have made some friends who are passionate about software development - it will make company easy to recruit talented people, and so on.

Don’t forget that we are paid to create working quality software - not to sit in front of PC and type.