WEBSITE

Building Out Of Browser Silverlight Applications

9/14/2010 4:41:16 PM

In the default deployment model for Silverlight, an application is delivered through the Silverlight plug-in embedded in a web page and consequently accessed through the user's choice of browser. In this scenario, the user must be connected to the web site that serves up the application.

Silverlight 3 extended that deployment model and introduced support for installing a Silverlight application on your local desktop. After the application is installed, you can use your platform's traditional mechanism to launch and run the application (for example, clicking an icon on the Start menu or desktop in Windows). This model of local installation is commonly known as the Out Of Browser (OOB) activation model for a Silverlight application.

In the process, you are no longer required to navigate to the application's source web site or open a browser window, nor do you have to be connected to a network. The application runs in its own window like any other installed application, providing the standard control mechanisms for the host window (close, minimize, maximize, and so on).

Silverlight 4 extends the OOB model in many ways. There is further control over the application's look and feel; for example, you can control aspects of the application window such as removing the default Windows chrome and supplying your own. You can also stipulate that the application be run with an elevated set of permissions that afford access to the local file system or the ability to interoperate with installed COM libraries through COM automation.

The recipes in this chapter show you how to take advantage of the OOB features of Silverlight, specifically:

  • Building an OOB application that can operate both in a connected and an offline mode

  • Controlling the application window characteristics and customizing the window chrome

  • Accessing the local file system

  • Interoperating with system services on Windows through COM Interop

  • Notification windows

Building a Silverlight application to run outside the browser

1. Problem

You need to give your Silverlight application the ability to be locally installed on a desktop. You also need the application to support execution with or without an available network connection.

2. Solution

Use the local installation support provided by Silverlight to enable the user to locally install the application. Use the network availability API in Silverlight to adapt your application logic to handle execution in an offline mode.

3. How It Works

3.1. Preparing the Application

The first step in enabling local installation for a Silverlight application is to supply the necessary installation settings in the Silverlight application manifest. Bring up the Project Properties page in Visual Studio for the Silverlight project, and you'll see an Enable running application out of the browser check box (see Figure 1). Check that option, and then click the Out-of-Browser Settings button to open the Out-of-Browser Settings dialog for the project (see Figure 2).

Figure 1. Enabling Out-of-Browser activation in Visual Studio

Figure 2. The Out-of-Browser Settings dialog

The Shortcut name field provides a user-friendly name for the application when it's installed on the desktop, and the Application description field provides a more detailed description. The Use GPU Acceleration check box specifies whether the locally installed application uses GPU acceleration (if available).

The installation process also requires that you provide four images, with square dimensions of 16, 32, 48 and 128 pixels each. These must be in PNG image format and must be included in the project with the Content setting specified for each image in Visual Studio. To select the appropriate image, click the adjoining Browse button to select from images included in your project, as shown in Figure 3. Note that you can choose not to specify these images, in which case the runtime uses a set of default images.

Figure 3. Selecting OOB icons

You also specify the initial Width, Height, and the Window Title of the host window within which the locally installed application launches. Note that these setting are the initial launch settings only, and the application always launches in a host window of these dimensions. Also note that the default initial location of the application window is centered on the screen, but if you check the Set window location manually option, you can specify the initial Top and Left coordinates of the application window in screen coordinates. Although the user can resize the host window when the application launches, Silverlight has no built-in facility to remember those settings across launches; however, we will show you in later recipes how to record these settings and control them programmatically. We will also discuss the other options on the dialog in later recipes.

The settings specified in this process are stored as XML in a file named OutOfBrowserSettings.xml under the Properties folder in your Silverlight project.

3.2. Installing the Application

If the installation settings are applied as discussed above, you can bring up the Silverlight context menu on the application running in the browser. You'll see an option to locally install the application, as shown in Figure 4.

Figure 4. Local installation menu option in the Silverlight context menu

Selecting this option opens an installation options dialog. Figure 5 shows a sample.

Figure 5. Installation options dialog for local installation of a Silverlight application

Note the text marked in bold in the dialog box. The title provided in the application identity settings in the application manifest is used to identify the application, and the web URI indicates the application's site of origin (in this case, http://localhost indicates that the application is being delivered from the local web server). The icon used in this dialog is the 128 × 128 pixel image provided in the application manifest.

After the application is installed, you can launch it directly from either the Start menu icon or the desktop shortcut added during the installation process, depending on your selection of the shortcut locations in the install dialog.

To remove a locally installed application, you can to run the application, either locally or in-browser by visiting the application web site, and bring up the Silverlight context menu. When the application is installed locally, the context menu offers an option to remove the application from your machine (see Figure 6). In Windows, you can also use Control Panel | Programs and Features to remove the listed application.

Figure 6. Context menu option for local application removal

3.3. Customizing the Installation Flow

The default mechanism of installing a Silverlight application through the context menu option may not always be a desirable choice. You may want to display a more visually appealing and slightly more obvious way of indicating to the user that the application can be locally installed. You may also want to have additional application logic tied to the process of the local installation. The System.Windows.Application class exposes some APIs to help control programmatic installation.

With the appropriate installation settings present in the application manifest as described earlier, invoking the static method Application.Install() from your application code has the same effect as invoking the context menu option for local installation.

The Application.InstallState property also gives you the current install state of the application. It is of the enumeration type System.Windows.InstallState and can have the following values:

  • NotInstalled: The current application has not been locally installed on the machine.

  • Installing: Either Application.Install() has been invoked or the user selected the install option from the context menu, and the application is about to be locally installed.

  • Installed: The currently running application is installed on the machine.

  • InstallFailed: An attempt to install the application was made, but the attempt failed.

The ApplicationInstallStateChanged event is raised whenever the value of Application.InstallState changes from one state to another in this list.

Note that the NotInstalled and Installed states are not necessarily indicative of the current application being run in or out of browser. For instance, if you install an application locally but navigate to the same application again on the same machine and load it in-browser from its site of origin, the InstallState of the in-browser application instance reports Installed. To know if your application is being launched locally or in-browser, rely on the Application.IsRunningOutOfBrowser static property of type Boolean; it returns true when the application is running locally and false when it is in-browser.

One obvious use of these APIs is to display different UIs to the user depending on the current install state. As an example, see the XAML in Listings 1 and 2. Note that we have left some portions out for brevity.

Listing 1. OnlinePage.xaml
<UserControl x:Class="Recipe8_1.OnlinePage"...>
<Grid>
...
<TextBlock Text="I am running in-browser".../>
<Button x:Name="btnInstall"
Content="Install Application"
Click="btnInstall_Click"/>
</Grid>
</UserControl>

Listing 2. LocalPage.xaml
<UserControl x:Class="Recipe8_1.LocalPage"...>
<Grid x:Name="LayoutRoot"
Background="White">
<TextBlock Text="I am running locally"/>
</Grid>
</UserControl>

Listing 1 shows a XAML page named OnlinePage.xaml that you want to display when the application is running in-browser. Listing 2 shows LocalPage.xaml, which you want the same application to display when running locally.

To detach the application, add the code shown here into the Click event handler of the Button named btnInstall in Listing 1:

private void btnInstall_Click(object sender, RoutedEventArgs e)
{
if(Application.Current.InstallState == InstallState.NotInstalled)
Application.Current.Install();
}

Check Application.InstallState; if it indicates that the code is currently running in-browser, you invoke the Install() method to locally install the application.

You also make an additional check in the Application.StartUp event handler and load the appropriate page:

private void Application_Startup(object sender, StartupEventArgs e)
{
if (Application.Current.IsRunningOutOfBrowser)
this.RootVisual = new LocalPage();
else
this.RootVisual = new OnlinePage();
}

We look at using the InstallState property and installation customization in more detail in this recipe's code sample.

3.4. Sensing Network Availability

When you are running a locally installed application, you may want to add application logic that allows the application to behave reasonably well in the absence of network connectivity.

Silverlight provides support in the framework for sensing network connectivity. As network state changes during the lifetime of your application, you can handle the static NetworkAddressChanged event in the System.Net.NetworkInformation.NetworkChange class to receive network-change notifications from the runtime. This event is raised any time any one of your computer's existing network interfaces goes through a network address change.

However, not all such notifications indicate unavailability of a network connection; they may indicate a transition from one valid network address to another. To determine if a valid network connection is available, in the event handler of the NetworkAddressChanged event (and anywhere else in your code), you can invoke the static GetIsNetworkAvailable() method in the System.Net.NetworkInformation.NetworkInterface class. This method returns true if an active network connection is available or false if not.

3.5. Updating Locally Installed Applications

The local installation deployment model also adds support for application-initiated self-updates for application code. The related API lets you check for updates to the application code at the site of origin and asynchronously download the changes.

The Application.CheckAndDownloadUpdateAsync() method checks for any updates to the application code at the site of origin. If it finds an updated version, the updated bits are downloaded to the local machine's application cache asynchronously. The Application.CheckAndDownloadUpdateCompleted event is raised when the download process completes or if the check reveals no changes. The CheckAndDownloadUpdatedCompletedEventArgs.UpdateAvailable property is set to truefalse if no updates were available. To apply the updates, the user needs to restart the application. if updates were downloaded or

Listing 3 shows a possible use of the application-update feature.

Listing 3. Code to Update an Application with Changes
private void Application_Startup(object sender, StartupEventArgs e)
{
if(Application.Current.InstallState == InstallState.Installed
&& Application.Current.IsRunningOutOfBrowser &&

NetworkInterface.GetIsNetworkAvailable())
{
Application.Current.CheckAndDownloadUpdateCompleted+=
new CheckAndDownloadUpdateCompletedEventHandler((s,args)=>
{
if (args.UpdateAvailable)
{
MessageBox.Show("New updates are available for this application." +
"Please restart the application to apply updates.","Update Status",
MessageBoxButton.OK);
this.RootVisual = new CheckUpdatePage();
}
else
this.RootVisual = new MainPage();
});
Application.Current.CheckAndDownloadUpdateAsync();

}
else
this.RootVisual = new MainPage();
}


As shown in Listing 8-3, you check to see if the application is running from a locally installed version out of the browser and has network connectivity. If so, you proceed to invoke CheckAndDownloadUpdatesAsync(). In the CheckAndDownloadUpdateCompleted handler, you check to see if updates are available. If there are updates, you display an appropriate message and use a different root visual to prevent the main application from running without the updates being applied.

Note that whether you want to enforce the download of an available update depends on application logic specified as shown in Listing 8-3. Should you choose to, you can let the user continue without applying the update, as long as your application can function as an older version without causing any errors.

4. The Code

The code sample for this recipe builds a simple note-taking application that allows the user to take notes that have a title and a body and stores them on the server categorized by the date the note was taken. The application can also be installed locally; the user can operate the locally installed application even when disconnected from the network by providing a local note store. The user can then synchronize the local note store with the server when network connectivity is restored.

Figure 7 shows the application two ways, running in-browser and locally installed.

Figure 7. NoteTaker application running in-browser and out of browser

The application displays the currently stored notes in a TreeView control, with the top-level nodes displaying the dates containing individual nodes for each note stored on that date. The user can use the buttons on the UI (from left to right) to install the application locally; synchronize any notes stored offline with the server version of notes data; create a new note; save a note; or remove a selected note, respectively. (Note that when you run the application locally, the install button is not displayed). The small Ellipse to the left of the buttons is colored green to indicate network availability and red otherwise.

A WCF service acts as the data source for the application. The WCF service uses the file system to store notes. Each note file is named with the unique ID of the note and is stored in a folder named after the date the note was created, along with other notes that have the same creation date.

Listing 4 shows the service contract definition as well as the data contract that defines the Note type.

Listing 4. Service and Data Contracts for the Note Manager WCF Service in INoteManager.cs
[ServiceContract]
public interface INoteManager
{
//Get all the dates for which we have notes stored
[OperationContract]
List<DateTime> GetDates();

//Get all the notes for a specific date
[OperationContract]
List<Note> GetNotesForDate(DateTime ForDate);

//Add a note to the note store
[OperationContract]
void AddNote(Note note);

//Remove a note from the note store
[OperationContract]
void RemoveNote(DateTime ForDate, string NoteID);
}

[DataContract]
public class Note
{
//Unique ID for the note
[DataMember]
public string NoteID { get; set; }
//When was the note created or last modified ?
[DataMember]
public DateTime LastModified { get; set; }
//When was the note last synchronized ?
[DataMember]
public DateTime? LastSynchronized { get; set; }
//Note title
[DataMember]
public string Title { get; set; }
//Note body
[DataMember]
public string Body { get; set; }
}


Let's look at the UI of the application in XAML before we discuss the code. Listing 5 shows relevant portions of the XAML for MainPage.xaml.

Listing 5. XAML for the NoteTaker Appli cation UI in MainPage.xaml
<UserControl
...
DataContext="{Binding RelativeSource={RelativeSource Self}}">
<UserControl.Resources>
<DataTemplate x:Key="dtNoteItem">
<Grid>
...
<Image
Source="/Recipe8_1.OfflineNoteTaker;component/images/Note.png".../>
<TextBlock Text="{Binding Path=Title}" .../>
</Grid>
</DataTemplate>

<windows:HierarchicalDataTemplate
ItemsSource="{Binding Path=Notes, Mode=OneWay}"
ItemTemplate="{StaticResource dtNoteItem}"

x:Key="dtDateItem">
<Grid>
...
<Image
Source="/ Recipe8_1.OfflineNoteTaker;component/images/Date.png".../>
<TextBlock Text="{Binding Path=Date}"... />
</Grid>
</windows:HierarchicalDataTemplate>

<local:BoolToVisibilityConverter x:Key="REF_BoolToVisibilityConverter" />
</UserControl.Resources>

<Grid x:Name="LayoutRoot"...>
...
<Grid ...>
...
<Button x:Name="btnInstall"
Click="btnInstall_Click"
Content="Install"
Visibility="{Binding
Converter={StaticResource REF_BoolToVisibilityConverter},
ConverterParameter=reverse, Mode=OneWay, Path=Installed}"...>
...
</Button>
<Button x:Name="btnSynchronize"
Click="btnSynchronize_Click"
Content="Synchronize"
Visibility="{Binding
Converter={StaticResource REF_BoolToVisibilityConverter},
Mode=OneWay, Path=NetworkOn}"...>
...
</Button>
<Button Content="New"
x:Name="btnNew"
Click="btnNew_Click"...>
...
</Button>
<Button Content="Save"
x:Name="btnSave"
Click="btnSave_Click"...>
...
</Button>
<Button x:Name="btnRemove"
Click="btnRemove_Click"
Content="Remove"...>


...
</Button>
</Grid>

<Grid...>
...
<TextBox x:Name="tbxTitle"
Text="{Binding Path=CurrentNote.Title, Mode=TwoWay}"
TextWrapping="NoWrap"...>
...
</TextBox>
<TextBox x:Name="tbxBody"
Text="{Binding Path=CurrentNote.Body, Mode=TwoWay}"
TextWrapping="Wrap"
AcceptsReturn="True"...>
...
</TextBox>
</Grid>
<controls:TreeView x:Name="NotesTree"
ItemsSource="{Binding Path=NotesByDate, Mode=OneWay}"
ItemTemplate="{StaticResource dtDateItem}"...>
...
</controls:TreeView>
<Grid ...>
<Ellipse x:Name="signNoNetwork"
Fill="#FFFF0000"
Visibility="{Binding Path=NetworkOn,Mode=OneWay,
Converter={StaticResource REF_BoolToVisibilityConverter},
ConverterParameter='reverse'}".../>
<Ellipse x:Name="signNetworkOn"
Fill="#FF75FF00"
Visibility="{Binding Path=NetworkOn,Mode=OneWay,
Converter={StaticResource REF_BoolToVisibilityConverter}}"... />
</Grid>
</Grid>
</UserControl>


The first thing to note in the XAML in Listing 5 is that the DataContext for the top level UserControl is bound to the MainPage code-behind class using the RelativeSource.Self enumerated value. This allows the rest of the UI to bind to properties defined directly on the MainPage class, without having to resort to defining separate data-class types for the most part.

The TreeView control instance named NotesTree displays currently stored notes. NotesTree.ItemsSource is bound to a property named NotesByDate of type ObservableCollection<TreeNodeData>, where TreeNodeData is a data class representing a top-level item in the TreeView. Listing 6 shows the TreeNodeData class.

Listing 6. TreeNodeData Data Class in MainPage.xaml.cs
//Represents a top level node (Date) in the tree view
//with children nodes (Note)
public class TreeNodeData : INotifyPropertyChanged
{
public event PropertyChangedEventHandler PropertyChanged;
private DateTime _Date = default(DateTime);
public DateTime Date
{
get
{
return _Date;
}
set
{
if (value != _Date)
{
_Date = value;
if (PropertyChanged != null)
PropertyChanged(this, new PropertyChangedEventArgs("Date"));
}
}
}

private ObservableCollection<Note> _Notes =
default(ObservableCollection<Note>);
public ObservableCollection<Note> Notes
{
get
{
return _Notes;
}
set
{
if (value != _Notes)
{
_Notes = value;
if (PropertyChanged != null)
PropertyChanged(this, new PropertyChangedEventArgs("Notes"));
}
}
}
}


Note that the TreeNodeData class is hierarchical in nature, in that each instance contains a Date property that defines the data at that level and a Notes property of type ObservableCollection <Note> that defines the data collection for the sublevel. Referring back to Listing 5, observe the use of the HierarchicalDataTemplate type to define the UI for the top-level nodes of NotesTree.

A HierarchicalDataTemplate is an extension of the data-template type meant to be used with hierarchical data sets such as the ones defined by a collection of TreeNodeData instances. It provides for data-template chaining that lets you define a data template for multiple levels of a hierarchical data set. In addition to binding a data item to a HierarchicalDataTemplate, you can set the ItemTemplate and ItemsSource properties of the template. The HierarchicalDataTemplate then applies the data template in the ItemTemplate property to each element in the collection bound to the ItemsSource.

In the example, dtDateItem is a HierarchicalDataTemplate containing the necessary XAML to display the dates as the top-level nodes; it is bound to TreeNodeData, defined in Listing 8-6. The ItemTemplate property on dtDateItem is set to use the dtNoteItem data template, whereas its ItemsSource is bound to the TreeNodeData.Notes property. This causes every Note instance in the Notes collection to use the dtNoteItem data template and be displayed as children to the corresponding date item in the TreeView.

Note that there is no system-enforced limit on this kind of chaining. Unlike in the example, if you need more levels in the hierarchy and you have a data structure that supports such nesting, you can use additional HierarchicalDataTemplates as children. When you reach a level at which you no longer need children items, you can resort to a simple DataTemplate.

The rest of the XAML is self-explanatory. The buttons on the UI serve different functions that we look at a moment when we explore the codebehind. The tbxTitletbxBody TextBoxes are bound to the Title and the Body properties of the CurrentNote property of the MainPage class, and the signNoNetwork and signNetworkOn ellipses are colored red and green and are both bound to the MainPage.NetworkOn property to be made visible conditionally depending on the value of the NetworkOn property. A ValueConverter converts bool to the Visibility type for these bindings. and

Before we look at the main application codebehind, let's cover one more aspect of the sample. Because the application is designed to work seamlessly even in the absence of a network connection, it needs an interface to store and retrieve note data from local storage when the WCF service cannot be reached. To facilitate that, you create a class called LocalNoteManagerClient, shown in Listing 7. This class mirrors the service contract used on the WCF service, but it implements all the note data-management functionality using the isolated storage feature in Silverlight.

Listing 7. LocalNoteManagerClient Class for Local Note Management
//Manages notes on the local client using Isolated Storage as the backing store
public class LocalNoteManagerClient
{
//gets all the dates
public List<DateTime> GetDates()
{
IsolatedStorageFile AppStore =
IsolatedStorageFile.GetUserStoreForApplication();
//get all the existing folders - each folder represents a date
//for which notes exist
string[] val = AppStore.GetDirectoryNames();
return AppStore.GetDirectoryNames().
Select((sz) => DateTime.Parse(sz.Replace("_","/"))).ToList();
}

//gets all the notes stored in local storage for a specific date
public ObservableCollection<Note> GetNotesForDate(DateTime ForDate)
{
ObservableCollection<Note> RetVal = new ObservableCollection<Note>();
IsolatedStorageFile AppStore =
IsolatedStorageFile.GetUserStoreForApplication();
//get the folder corresponding to this date
string DirPath = ForDate.ToShortDateString().Replace("/", "_");
//if folder exists
if (AppStore.DirectoryExists(DirPath))
{
//get all the files
string[] FileNames = AppStore.
GetFileNames(System.IO.Path.Combine(DirPath, "*.note"));
foreach (string FileName in FileNames)
{
//open a file
IsolatedStorageFileStream fs = AppStore.
OpenFile(System.IO.Path.Combine(DirPath, FileName), FileMode.Open);
//deserialize
DataContractJsonSerializer serNote =
new DataContractJsonSerializer(typeof(Note));
//add to returned collection
RetVal.Add(serNote.ReadObject(fs) as Note);
//close file
fs.Close();
}
}
//return collection
return RetVal;
}
//adds a note to local storage
public void AddNote(Note note)
{
IsolatedStorageFile AppStore =
IsolatedStorageFile.GetUserStoreForApplication();
string DirPath = note.LastModified.ToShortDateString().Replace("/", "_");
//if a directory for the note date does not exist - create one
if (AppStore.DirectoryExists(DirPath) == false)
AppStore.CreateDirectory(DirPath);
string FilePath = string.Format("{0}\\{1}",
DirPath, note.NoteID + ".note");
//create file, serialize and store
IsolatedStorageFileStream fs = AppStore.
OpenFile(FilePath, FileMode.Create);


DataContractJsonSerializer serNote =
new DataContractJsonSerializer(typeof(Note));
serNote.WriteObject(fs, note);
fs.Close();
}
//removes a note from local storage
public void RemoveNote(DateTime ForDate, string NoteID)
{
IsolatedStorageFile AppStore =
IsolatedStorageFile.GetUserStoreForApplication();
string FilePath = string.Format("{0}\\{1}",
ForDate.ToShortDateString().Replace("/", "_"), NoteID + ".note") ;

if (AppStore.FileExists(FilePath))
AppStore.DeleteFile(FilePath);
}
}

Now, let's look at the main application functionality, most of which is in the MainPage.xaml.cs codebehind class. Listing 8 shows the MainPage class.

Listing 8. MainPage.xaml.cs Codebehind Class for the Offline NoteTaker
public partial class MainPage : UserControl, INotifyPropertyChanged
{
public event PropertyChangedEventHandler PropertyChanged;
//initialize to a blank note
private Note _CurrentNote = new Note()
{
NoteID = Guid.NewGuid().ToString(),
LastModified = DateTime.Now
};

//Tracks the currently selected/displayed note
public Note CurrentNote
{
get { return _CurrentNote; }
set
{
if (value != _CurrentNote)
{
_CurrentNote = value;
if (PropertyChanged != null)
PropertyChanged(this, new PropertyChangedEventArgs("CurrentNote"));
}
}

}

private ObservableCollection<TreeNodeData> _NotesByDate =
default(ObservableCollection<TreeNodeData>);
//Collection of TreeNodeData that binds to the TreeView to display saved notes
public ObservableCollection<TreeNodeData> NotesByDate
{
get
{
//initialize to a blank collection
if (_NotesByDate == null)
_NotesByDate = new ObservableCollection<TreeNodeData>();
return _NotesByDate;
}
set
{
if (value != _NotesByDate)
{
_NotesByDate = value;
if (PropertyChanged != null)
PropertyChanged(this, new PropertyChangedEventArgs("NotesByDate"));
}
}
}

//Indicates if the app is running offline - used to bind to XAML
public bool Installed
{
get
{
return Application.Current.InstallState == InstallState.Installed;
}
}
//Indicates if network connectivity is available - used to bind to XAML
public bool NetworkOn
{
get
{
return NetworkInterface.GetIsNetworkAvailable();
}
}

public MainPage()
{
InitializeComponent();

//Refresh notes treeview
RefreshNotesView();
//listen for network connection/disconnection events
NetworkChange.NetworkAddressChanged +=
new NetworkAddressChangedEventHandler((s, a) =>
{
//update XAML bound property
if (PropertyChanged != null)
PropertyChanged(this, new PropertyChangedEventArgs("NetworkOn"));
//refersh the treeview to display remote/local notes appropriately
RefreshNotesView();
});
//handle selection change in the notes treeview
NotesTree.SelectedItemChanged +=
new RoutedPropertyChangedEventHandler<object>((s, a) =>
{
if (a.NewValue is Note)
{
//set the CurrentNote property to the currently selected note
CurrentNote = a.NewValue as Note;
}
});
}

//take the application offline
private void btnInstall_Click(object sender, RoutedEventArgs e)
{
Application.Current.Install();
}
private void RefreshNotesView()
{
//clear current bound collection
NotesByDate.Clear();
//reinitialize the CurrentNote
CurrentNote = new Note()
{
NoteID = Guid.NewGuid().ToString(),
LastModified = DateTime.Now
};
//if we have network connectivity
if (NetworkOn)
{
//use the WCF proxy
NoteManagerClient client = new NoteManagerClient();
//handle getting all the dates asynchronously

client.GetDatesCompleted +=
new EventHandler<GetDatesCompletedEventArgs>((sender, args) =>
{

foreach (DateTime dt in args.Result)
{
//create another instance of the WCF proxy
NoteManagerClient client1 = new NoteManagerClient();
//handle getting the notes for a date asynchronously
client1.GetNotesForDateCompleted +=
new EventHandler<GetNotesForDateCompletedEventArgs>((s, a) =>
{
//create a node for the date and add the notes to it
NotesByDate.Add(
new TreeNodeData()
{
Date = (DateTime)a.UserState,
Notes = new ObservableCollection<Note>(a.Result)
});
});
//get all the notes on the server for a specific date
//pass in the date as user state
client1.GetNotesForDateAsync(dt, dt);
}
});
//get all the dates for which we have notes on the server
client.GetDatesAsync();
}
else
{
//create a client for local note management
LocalNoteManagerClient client = new LocalNoteManagerClient();
//Get all the dates
List<DateTime> dates = client.GetDates();
foreach (DateTime dt in dates)
{
//get the notes for that date
ObservableCollection<Note> notesForDate = client.GetNotesForDate(dt);
//add to the treeview
NotesByDate.Add(
new TreeNodeData()
{
Date = dt,
Notes = notesForDate
});

}
}
}

//handle the Save button
private void btnSave_Click(object sender, RoutedEventArgs e)
{
if (NetworkOn)
{
//use the WCF proxy
NoteManagerClient client = new NoteManagerClient();
client.AddNoteCompleted +=
new EventHandler<AsyncCompletedEventArgs>((s, a) =>
{
//refresh the treeview
RefreshNotesView();
});
//add the new/updated note to the server
client.AddNoteAsync(CurrentNote);
}
else
{
//use the local note manager
LocalNoteManagerClient client = new LocalNoteManagerClient();
//add the note
client.AddNote(CurrentNote);
//refresh the tree view
RefreshNotesView();
}
}

//handle the New Button
private void btnNew_Click(object sender, RoutedEventArgs e)
{
//reinitialize the CurrentNote
CurrentNote = new Note()
{
NoteID = Guid.NewGuid().ToString(),
LastModified = DateTime.Now
};
}

//handle Remove button
private void btnRemove_Click(object sender, RoutedEventArgs e)
{

//a valid existing note has to be selected
if (CurrentNote == null ||
NotesByDate.SelectMany((tnd) => tnd.Notes).
Where((nt) => nt == CurrentNote).Count() > 0)
return;

if (NetworkOn)
{
//use the WCF proxy
NoteManagerClient remoteClient = new NoteManagerClient();
remoteClient.RemoveNoteCompleted +=
new EventHandler<AsyncCompletedEventArgs>((s, a) =>
{
//refresh tree view
RefreshNotesView();
});
//remove the note
remoteClient.RemoveNoteAsync(CurrentNote.LastModified, CurrentNote.NoteID);
}
else
{
//use the local client
LocalNoteManagerClient localClient = new LocalNoteManagerClient();
//remove note
localClient.RemoveNote(CurrentNote.LastModified, CurrentNote.NoteID);
//refresh tree view
RefreshNotesView();
}
}

//handle Synchronize button
private void btnSynchronize_Click(object sender, RoutedEventArgs e)
{
SynchronizeOfflineStore();
}

private void SynchronizeOfflineStore()
{
LocalNoteManagerClient localClient = new LocalNoteManagerClient();
//Notes that are on the server with LastModifiedDate <= LastSynchronizedDate
//but are missing on the client, must have been deleted on the client
List<Note> NotesDeletedOnClient =
NotesByDate.SelectMany((tnd) => tnd.Notes).Distinct().
Where((nt) => nt.LastSynchronized >= nt.LastModified).
Except(localClient.GetDates().

SelectMany((dt) => localClient.GetNotesForDate(dt)).
Distinct()).ToList();
//remove the deleted notes from the server
foreach (Note nt in NotesDeletedOnClient)
{
NoteManagerClient remoteClient = new NoteManagerClient();
remoteClient.RemoveNoteAsync(nt.LastModified, nt.NoteID);
}
//Notes that are on the client with LastModifiedDate <= LastSynchronizedDate
//but are missing on the server, must have been deleted on the server
List<Note> NotesDeletedOnServer =
localClient.GetDates().
SelectMany((dt) => localClient.GetNotesForDate(dt)).Distinct().
Where((nt) => nt.LastSynchronized >= nt.LastModified).Except(
NotesByDate.SelectMany((tnd) => tnd.Notes).Distinct()).ToList();
//remove the deleted notes from the client
foreach (Note nt in NotesDeletedOnServer)
localClient.RemoveNote(nt.LastModified, nt.NoteID);
//get all the notes on the server that have not been synchronized with the
//client. Since we are online, the notes represented in NotesByDate
//constitutes the server state
List<Note> NotesOutOfSyncOnServer =
NotesByDate.SelectMany((tnd) => tnd.Notes).Distinct().
Where((nt) => nt.LastSynchronized == null ||
nt.LastSynchronized < nt.LastModified).ToList();
//add the server side notes to the client
foreach (Note nt in NotesOutOfSyncOnServer)
{
//set appropriate timestamps
nt.LastSynchronized = DateTime.Now;
nt.LastModified = nt.LastSynchronized.Value;
localClient.AddNote(nt);
}
//get all the notes on the client that have not been synchronized with the
//server.
List<Note> NotesOutOfSyncOnClient =
localClient.GetDates().
SelectMany((dt) => localClient.GetNotesForDate(dt)).Distinct().
Where((nt) => nt.LastSynchronized == null ||
nt.LastSynchronized < nt.LastModified).ToList();

//add the client side notes to the server
foreach (Note nt in NotesOutOfSyncOnClient)
{
NoteManagerClient remoteClient = new NoteManagerClient();


//timestamps
nt.LastSynchronized = DateTime.Now;
nt.LastModified = nt.LastSynchronized.Value;
remoteClient.AddNoteAsync(nt);
}
//refresh
RefreshNotesView();
}
}

The MainPage class defines a few properties that are noteworthy. The NotesByDate property of type ObservableCollection<TreeNodeData> defines the entire note collection at any point in time, and the CurrentNote property defines the currently selected note in the UI. The Installed property wraps around Application.InstallState and returns true if its value is InstallState.Installed. The NetworkOn property wraps a call to NetworkInterface.GetIsNetworkAvailable() to indicate network availability.

You use the RefreshNotesView() method to load any existing notes in the constructor of the page. As shown in the definition of RefreshNotesView() in Listing 8-8, the NetworkOn property determines network availability. If a network connection is available, you use the WCF service proxy to access the note data and populate the NotesByDate collection, which in turn displays the data in the NotesTree TreeView. In the absence of a network connection, you use the LocalNoteManagerClient class to access the data from local storage and use it similarly.

NOTE

To create a network-unavailable state in your system, the easiest option is to turn off your network adapter. If you have multiple adapters on and connected, make sure you turn all of them off.

In the constructor, you also handle a few events. You attach a handler to the NetworkChange.NetworkAddressChanged event; in the event of a network state change, you update the UI by raising the PropertyChanged event and invoke RefreshNotesView() again to acquire the note data from the appropriate storage location. You also handle the SelectedItemChanged event on the NotesTree TreeView control to set the value of the CurrentNote property to the currently selected note.

The handlers for the Click events on btnSave, btnRemove, btnNew, and btnInstall are straightforward. In each of the first three handlers, you again use either the WCF service or local storage, depending on the state of network availability. And btnInstall_Click() is a simple wrapper to an invocation of Application.Install() that takes you through the local installation process, as described in the previous section.

The last piece of this recipe is the data-synchronization logic. Before we delve into it, note that this is merely a sample and the synchronization logic demonstrated here is implemented from scratch. If you are building a sizeable application, you should investigate other scalable and robust data-synchronization frameworks like the Microsoft Sync Framework. You can find more information about the Sync Framework at msdn.microsoft.com/en-us/sync/default.aspx.

The synchronization logic in this sample is invoked through handling the Click event of the btnSynchronize button on the UI and is encapsulated in the SynchronizeOfflineStore() method. Because the Visibility property of btnSynchronize is tied to network availability through a binding to the NetworkOn property, you are assured that this code is invoked only when the network is available.

The synchronization logic in SynchronizeOfflineStore() is straightforward. You first use the LastModifiedDate and LastSynchronizedDate properties on the Note instances to look for notes that have been deleted on one side of storage but still exist on the other side. The logic is simple: if a note exists on one side and has been synchronized more recently than it has been modified, but it does not exist on the other side, then it must have been deleted from the side on which it does not exist. You then delete that note from the side on which it currently exists.

Next, you look for notes on either side with a modification date more recent than the last synchronization date. These notes have been either added or updated, and the changes have not been synchronized. You invoke AddNote() on the appropriate storage service contract for these notes. The implementation of AddNote() on the WCF service and on LocalNoteManagerClient always creates a new note. If the data synchronization required an update of a note with partial changes to its data on one side, the complete note file is written again, but in effect it provides the desired result.

This takes care of propagating all the changes bi-directionally. On completion of this method, both data stores are synchronized.

Other  
 
Most View
Wake Up Your Wi-Fi (Part 1)
Give Your PC A Shop-Fresh Feel
Back To School - The iOS Study Companion (Part 1)
The Apple iPad (Fourth Generation) - The Bigger Brother Is Back
Chromebook Pixel - Google’s Unreal Wonder (Part 2)
Asus Taichi 21 Review – Are 2 Screens Better Than One? (Part 2)
The Return Of The Mini PC
Windows Server 2008 and Windows Vista : GPMC Scripts - GPO Reporting (part 1)
Stop SPAM, Save Time
Build Your Own Mini-ITX Marvel (Part 1)
Top 10
Sharepoint 2013 : Farm Management - Disable a Timer Job,Start a Timer Job, Set the Schedule for a Timer Job
Sharepoint 2013 : Farm Management - Display Available Timer Jobs on the Farm, Get a Specific Timer Job, Enable a Timer Job
Sharepoint 2013 : Farm Management - Review Workflow Configuration Settings,Modify Workflow Configuration Settings
Sharepoint 2013 : Farm Management - Review SharePoint Designer Settings, Configure SharePoint Designer Settings
Sharepoint 2013 : Farm Management - Remove a Managed Path, Merge Log Files, End the Current Log File
SQL Server 2012 : Policy Based Management - Evaluating Policies
SQL Server 2012 : Defining Policies (part 3) - Creating Policies
SQL Server 2012 : Defining Policies (part 2) - Conditions
SQL Server 2012 : Defining Policies (part 1) - Management Facets
Microsoft Exchange Server 2010 : Configuring Anti-Spam and Message Filtering Options (part 4) - Preventing Internal Servers from Being Filtered