Saturday, October 15, 2011

How to Create a Simple Silverlight application

Prerequisites

QuickStart

If you want to see the end result of this tutorial and you have installed all the prerequisites, then please download the ZIP file below, unzip it, deploy it to a web sever, and navigate to sloob.html in your browser.
You can also see it in action, here.
Step 1: Create the HTML & JS files
The HTML file, named sloob.html, will look the same as usual, but it now references the new Silverlight.js file found in the Silverlight 3.0 SDK Tools directory.

<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en">
     <head>
          <title>Silverlight Out-of-Browser Test</title>
          <script type="text/javascript" src="Silverlight.js"></script>
          <script type="text/javascript" src="createSilverlight.js"></script>
      </head>
      <body>
          <div id="silverlightControlHost">
          </div>
          <script type="text/javascript">
                // Find the div by id
                var hostElement = document.getElementById("silverlightControlHost");

                  // Create the Silverlight control
                createSilverlight(hostElement);
           </script>
      </body>
</html>
The Javascript file, createSilverlight.js, contains two key changes: a version number updated for Silverlight 3 and a flag to disable auto upgrade.

//creates the silverlight control within the tag specified by controlHostId

function createSilverlight( controlHost )
{
    Silverlight.createObjectEx({
        source: "ClientBin/SloobApplication.xap",
        parentElement: controlHost,
        id: "silverlightControl",
        properties: {
            width: "500",
            height: "350",
            version: "3.0.40307.0",
            background: "white",
            isWindowless: "true",
            enableHtmlAccess: "true",
            autoUpgrade: "false"
        },
        events: {}
    });
}
The “autoUpgrade” flag instructs Silverlight to try and automatically upgrade a user’s Silverlight run-time if they’re running a lower version than the one specified. For the Silverlight 3 Beta, we leave this set to false to avoid the “infinite install loop” as detailed by Tim Heuer at Method of Failed.
Step 2: Create the XAML file
The XAML consists of several TextBlocks that display the launch location, running state and network status of the application, a modifiable network connection validity URL inside a TextBox and buttons to initiate a network status check and desktop install. The usage of these components will become clear as you read through the tutorial, but for now, create a file named “SloobControl.xaml” and put this code in it:

<UserControl xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" x:Class="SloobApplication.SloobControl">
    <Grid>
        <Grid.RowDefinitions>
            <RowDefinition Height="40" />
            <RowDefinition Height="40" />
            <RowDefinition Height="40" />
            <RowDefinition Height="40" />
            <RowDefinition Height="30" />
            <RowDefinition Height="30" />
        </Grid.RowDefinitions>
        <Grid.ColumnDefinitions>
            <ColumnDefinition Width="130" />
            <ColumnDefinition Width="300" />
        </Grid.ColumnDefinitions>

        <TextBlock Text="Launch Location:" Width="110" Height="35" Grid.Column="0" Grid.Row="0" />
        <TextBlock Text="Running State:" Width="110" Height="35" Grid.Column="0" Grid.Row="1" />
        <TextBlock Text="Network Status:" Width="110" Height="35" Grid.Column="0" Grid.Row="2" />
        <TextBlock Text="Network Status Url:" Width="110" Height="35" Grid.Column="0" Grid.Row="3" />

        <TextBlock x:Name="LaunchLocation" Text="" Width="280" Height="35" Grid.Column="1" Grid.Row="0" />
        <TextBlock x:Name="RunningState" Text="" Width="280" Height="35" Grid.Column="1" Grid.Row="1" />
        <TextBlock x:Name="NetworkStatus" Text="" Width="280" Height="35" Grid.Column="1" Grid.Row="2" />
        <TextBox x:Name="NetworkStatusUrl" Text="" Width="280" Height="25" Grid.Column="1" Grid.Row="3" />
        <Button x:Name="CheckNetworkStatus" Content="Check Network Status" Click="OnCheckNetworkStatusClicked" Width="280" Height="25" Grid.Column="1" Grid.Row="4" />
        <Button x:Name="Install" Content="Install" Click="OnInstallClicked" Width="280" Height="25" Grid.Column="1" Grid.Row="5" />
    </Grid>
</UserControl>
Now let’s put these components to use.
Step 3: Add location detection logic
We start with a class named SloobControl in “SloobControl.xaml.cs”:

namespace SloobApplication
{
    using System;
    using System.IO;
    using System.Net;
    using System.Net.NetworkInformation;
    using System.Text;
    using System.Windows;
    using System.Windows.Controls;
    using System.Xml;
    using System.Xml.Linq;

    public partial class SloobControl : UserControl
    {
        public SloobControl()
        {
                    InitializeComponent();
        }
    }
}
To this class, we’ll add a new function named DetectLocation which contains our location detection logic:

        private void DetectLaunchLocation()
        {
            if( Application.Current.ExecutionState == ExecutionStates.RunningOnline )
            {
                LaunchLocation.Text = "Launched in a browser.";
            }
            else if( Application.Current.RunningOffline )
            {
                LaunchLocation.Text = "Launched from the desktop.";
            }
            else if( Application.Current.ExecutionState == ExecutionStates.Detached )
            {
                LaunchLocation.Text = "Launched in browser.";
            }
        }
At first glance, this code appears deceptively simple. It checks the state of a couple of application variables and updates the appropriate TextBlock. The confusion comes in knowing which variable to check for the status you want to know. The ExecutionState property in the first if branch will contain different values that change over time depending on whether or not the application gets installed on the desktop. Here, ExecutionState will equal ExecutionStates.RunningOnline if the user launches the Silverlight application from the browser AND does NOT have it installed on the desktop. If the user launches the application from the desktop, then the RunningOffline property will equal true and the code takes the second branch. If the user launches the Silverlight application in a browser AND they DO have it installed on the desktop, Silverlight considers it Detached and the code falls to the last branch. This means that if the user has a Silverlight application installed, the Silverlight runtime will ALWAYS load the application from disk cache rather than downloading it from the web server irrespective of how the user access it (either from the desktop or the web browser). I find this peculiar…why not just access the latest version from the browser in that case? We’ll see if that behavior makes it into the final Silverlight 3 runtime.
We should detect the launch location on load, so let’s add a call to DetectLocation in the constructor:

        public SloobControl()
        {
                    InitializeComponent();

                    DetectLaunchLocation();
        }
Knowing how a user launched a Silverlight application doesn’t tell everything because the execution state of a Silverlight application can change over time. So, let’s track that.
Step 4: Add running state logic
Similar to the previous step, let’s add a function called DetectRunningState:

    public partial class SloobControl : UserControl
    {
        private bool detaching = false;

        public SloobControl()
        {
        ...
        }

        private void DetectRunningState()
        {
            if( Application.Current.ExecutionState == ExecutionStates.RunningOnline )
            {
                RunningState.Text = "Running in browser.";
            }           
            else if( Application.Current.ExecutionState == ExecutionStates.Detaching )
            {
                this.detaching = true;
                RunningState.Text = "Detaching from browser.";
            }
            else if( Application.Current.ExecutionState == ExecutionStates.Detached )
            {
                if( this.detaching )
                {
                    this.detaching = false;
                    RunningState.Text += "...complete.";
                }
                else
                    RunningState.Text = "Running from cache.";
            }
            else if( Application.Current.ExecutionState == ExecutionStates.DetachedUpdatesAvailable )
                RunningState.Text = "Running from cache but updates are available.";
            else if( Application.Current.ExecutionState == ExecutionStates.DetachFailed )
                RunningState.Text = "Unable to detached from browser.";
        }
More conditional logic, yay! The first branch should look familiar. Here, an execution state of ExecutionStates.RunningOnline means the Silverlight application currently runs in the browser (note, in this function RunningState TextBlock gets updated with the current execution state text). The code reaches the next branch that checks for ExecutionStates.Detaching whenever the user triggers a desktop installation of the application. By setting the “detaching” variable to true, the next branch which looks for ExecutionStates.Detached will know if a desktop install just completed or if the application launched from the cache (remember, the application will ALWAYS launch from the cache if the user has it installed on the desktop, even if they navigate back the the original site they installed the application from). Application.Current.ExecutionState will equal ExecutionStates.DetachedUpdatesAvailable when the Silverlight application runs from the cache, but the runtime detects updates to the application’s XAP file. Tim Heuer wrote an execllent article that walks through the Silverlight offline update logic on his Method of Failed blog, but in essence the Silverlight runtime sends an HTTP request with the If-Modified-Since header populated. If it gets back an HTTP response of 304 Not Modified, then it assumes it has an up-to-date XAP file. And finally, for completeness, the final branch notifies the user if an attempted detach (i.e. an attempt to install the application to the desktop) failed.
If the execution state changes over time, that means Silverlight must send us a notification when a change occurs. This happens when the Silverlight runtime raises the Application.Current.ExecutionStateChanged event. So, we should add code to respond to this event, like so:

    public partial class SloobControl : UserControl
    {
        private bool detaching = false;
        private bool checkingNetwork = true;

        public SloobControl()
        {
            InitializeComponent();

            Application.Current.ExecutionStateChanged += OnExecutionStateChanged;
           
            DetectLaunchLocation();
            DetectRunningState();
        }

        void OnExecutionStateChanged(object sender, EventArgs e)
        {
            DetectRunningState();
        }
The first highlighted line in the constructor shows where the OnExecutionStateChanged gets added to the ExecutionStateChanged event. Since this event doesn’t raise on initialization, I also added a call to DetectRunningState. Finally, the OnExecutionStateChanged function does nothing more than call DetectRunningState itself.
Now for the hard part. ;)
Step 5: Add network status logic
As Peter Smith noted in his Silverlight presentation at Mix ‘09 to insure that your Silverlight application has access to the Internet you should actually attempt to download and verify bits. He also wrote two articles for the Network Class Library Team blog for general online/offline detection in .NET and different code patterns to use for offline detection in Silverlight. For simplicity, I will only implement a bare bones method here, so I urge anyone needing to implement this feature to read both of these posts.
Like usual, let’s start with a function named DetectNetworkStatus:

    public partial class SloobControl : UserControl
    {
        private const string defaultUrl = "http://www.dieajax.com/downloads/tutorial/silverlight/sloob/test.xml";
        private WebClient webClient = new WebClient();
        private bool checkingNetwork = true;

        public SloobControl()
        {
            InitializeComponent();

            NetworkStatusUrl.Text = defaultUrl;
         
            ...
        }

        private void DetectNetworkStatus()
        {           
            bool networkAvailable = NetworkInterface.GetIsNetworkAvailable();

            if( networkAvailable )
            {
                NetworkStatus.Text = "Determining network status...";

                string networkStatusUrl = NetworkStatusUrl.Text + "?FoolCache=" + DateTime.Now.ToString();
                webClient.DownloadStringAsync(new Uri(networkStatusUrl , UriKind.Absolute));
                this.checkingNetwork = true;
                CheckNetworkStatus.Content = "Cancel Network Status Check";
            }
            else
                this.ShowNetworkStatus( false, null );
        }

        private void ShowNetworkStatus( bool online, String additionalInfo )
        {
            if( online )
                NetworkStatus.Text = "Connected to network.";
            else
                NetworkStatus.Text = "Disconnected from network.";

            if( additionalInfo != null )
                NetworkStatus.Text += "\n" + additionalInfo;
        }
The call to NetworkInterface.GetIsNetworkAvailable checks for network connectivity, and if it returns true, then the DownloadStringAsync function of the WebClient class attempts to download the resource at the URL in the “NetworkStatusUrl” TextBox. Being an asynchronous call, DownloadStringAsync returns immediately and for tracking purposes the “checkingNetwork” variable gets set to true and the “CheckNetworkStatus” button text changes to let the user know that another button press will cancel the asynchronous download currently in progress.
You may have noticed the “FoolCache” get variable appended to the URL before the call to DownloadStringAsync. Like checking the XAP file for updates, for efficiency’s sake, the browser sets the If-Modified-Since header when doing the HTTP call to the URL. If the file hasn’t changed, the browser just pulls the file from its cache and hands it to Silverlight. However, it appears my web server doesn’t respect that, meaning that even if I change the file on the web server, the browser, and thus Silverlight, will continue to pull the file from the cache. As recommened by Microsoft employee, I attempted to manually set the If-Modified-Since HTTP header to a much older value, forcing the browser to always think a newer file exists on the server, but I got a “restricted header” exception from the WebClient class. Finally, I decided to use a unique URL every time via the “FoolCache” HTTP GET parameter which, while being more hack-ish, also fools the browser into always getting the file. A few others have implemented a similar solution:
I should also note that I couldn’t get the XAP file auto-update working either because it uses the same mechanism. In the future, I hope Microsoft will give developers an easy way to customize when to update an installed Silverlight application.
Finally, the ShowNetworkStatus function simply wraps some common display logic.
When the WebClient finishes downloading or cancels a download, it raises an event that we should handle:

        public SloobControl()
        {
            ...
            webClient.DownloadStringCompleted += new DownloadStringCompletedEventHandler(OnDownloadStringCompleted);
            ...
        }

        private void OnDownloadStringCompleted(object sender, DownloadStringCompletedEventArgs e)
        {
            if( !e.Cancelled )
            {
                if( e.Error == null )
                {
                    XDocument doc = XDocument.Parse( e.Result );

                    if( (doc != null ) && (doc.Element( "test" ) != null) )
                        this.ShowNetworkStatus( true, null );
                    else
                        this.ShowNetworkStatus( false, "Error retreiving verification data." );
                }
                else
                {
                    this.ShowNetworkStatus( false, "Error contacting verification site." );
                }
            }
            else
                NetworkStatus.Text = "Cancelled.";

            this.checkingNetwork = false;
            CheckNetworkStatus.Content = "Check Network Status";
        }
In the constructor, you can see that the OnDownloadStringCompleted function handles the DownloadStringCompleted event. This function first checks to see if the user cancelled the download operation. If not, it attempts to parse the XML data it should have gotten from the request. If it parses correctly, then it assumes it has a valid Internet connection and updates the display accordingly.
Step 6: Trigger network status checks
This application has three scenarios for when it should check network status:
  1. When the OS detectes a network address change
  2. On application start
  3. When the user clicks on the Check Network Status button
Let’s add code to handle each of these cases:

        public SloobControl()
        {
            ...
            NetworkChange.NetworkAddressChanged += OnNetworkAddressChanged;
            ...
            DetectNetworkStatus();
        }

        void OnNetworkAddressChanged( object sender, EventArgs args )
        {
            if( !this.checkingNetwork )
                DetectNetworkStatus();
        }

        void OnCheckNetworkStatusClicked( object sender, RoutedEventArgs args )
        {
            if( this.checkingNetwork )
            {
                webClient.CancelAsync();
            }
            else
            {
                DetectNetworkStatus();
            }
        }
The first line in the constructor adds the OnNetworkAddressChanged event handler to the NetworkChange.NetworkAddressChanged event that comes from the OS. If a network status check isn’t currently happening, the OnNetworkAddressChanged function simply delegates to the DetectNetworkStatus function we created in the previous step. If you refer back to the XAML, you will see that the “CheckNetworkStatus” button has a click handler funtion named OnCheckNetworkStatusClicked. This function first determines if the application has a network check in progress and if so, cancels it. If not, it just calls the DetectNetworkStatus function.
Step 7: Programmatically trigger an install
Of course, the user can always install any Silverlight application that has out-of-browser support by right-clicking on the application and selecting the “Install xxxx onto this computer” menu item, where “xxxx” represents the name of the applicaiton. However, Microsoft also provided a way to install applications programmatically. I should note that application install can only occur as a result of a user action, for example, a button press. Trying to install an application on the constructor won’t work. With that in mind, I added a button named “Install” in the XAML with a click event handler function named OnInstallClicked. The logic for that function looks like this:

        void OnInstallClicked( object sender, RoutedEventArgs args )
        {
            Application.Current.Detach();
        }
Thankfully, Microsoft made the install process very simple: one function call brings up a dialog box asking the user if they want to install your application. The actual install process looks a bit different depending on which operating system you use and Channel 9 has an excellent video showing the differences between installing Silverlight on Windows and Mac OS X.
Step 8: Build it
The sample code bundle contains the project file and all the support files needed to build this sample. The build file looks similar to the build file in my Silverlight MSBuild tutorial with the exception of a few changes to the project file and application manifest:
SloobApplication.csproj

<ItemGroup>
  <Reference Include="mscorlib" />
  <Reference Include="system" />
  <Reference Include="system.Core" />
  <Reference Include="System.Windows" />
  <Reference Include="System.Net" />
  <Reference Include="System.Xml" />
  <Reference Include="System.Xml.Linq" />
  <Reference Include="System.Windows.Browser" />
</ItemGroup>
.…
<Import Project="$(MSBuildExtensionsPath)\Microsoft\Silverlight\v3.0\Microsoft.Silverlight.CSharp.targets" />
The first few highlighted lines point out the assemblies I added for network, XML and LINQ support. The last highlighted line shows that the project now references the new Silverlight 3 build targets.
AppManifest.xml

<Deployment xmlns="http://schemas.microsoft.com/client/2007/deployment" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
    <Deployment.ApplicationIdentity>
        <ApplicationIdentity
            ShortName="SLOOB Test"
            Title="Silverlight 3 Out-Of-Browser Test">
            <ApplicationIdentity.Blurb>
        Tests out-of-browser functionality in Silverlight 3
            </ApplicationIdentity.Blurb>
        </ApplicationIdentity>
    </Deployment.ApplicationIdentity>

</Deployment>
The AppMainfest file contains the desktop installation magic. The Deployment.ApplicationIdentity tag tells the Silverlight runtime that this application supports out-of-browser deployment. The “ShortName” tag will appear when the user right-clicks on the application as I noted in the previous step. Since deployment only requires changes to the application manifest, you can enable Silverlight application for desktop installs without having to recompile. Way to go Microsoft!
You can build the application with this command:

"C:\WINDOWS\Microsoft.NET\Framework\v3.5\msbuild.exe" SloobApplication.csproj
Step 9: Run it
Normally, I test my Silverlight applications by simply opening the HTML file locally, but for this tutorial, XAP update checks and network status checks won’t work if you run it locally. Go ahead and play around with installing, launching and removing the application and watch how the text statuses change. For me, it remained very unintuitive at first, however, now I pretty much have a handle on how Silverlight works with cached/installed applications…and it almost kinda makes sense. 

Conclusion

So, that’s Silverlight out-of-browser support. To be honest, it feels a little last minute to me. Or perhaps Microsoft simply decided not to over reach and do the bare minimum to have feature parity with JavaFX. Although, I must admit my disappointment at not seeing any cool “drag to install” (no pun intended ) demos like I’ve seen for JavaFX. I assume Silverlight 3 won’t support functionality like this upon release, but hopefully it’ll come in a later version.
I did however discover something very promising while perusing the new Silverlight doucmentation: it looks like Microsoft finally realizes that people want to host Silverlight outside of the browser and will officially support this (to an extent). The Mono guys have focused on Silverlight “desklets” from day one, so I makes me happy to see Microsoft finally moving towards doing the same. Silverlight out-of-browser support definitely feels like a tacked-on, “checkbox” feature this release, but I remain excitied to see how it evolves in the future.
Share and Enjoy:

No comments:

Post a Comment