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).
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.
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.
Selecting this option opens an installation options dialog. Figure 5 shows a sample.
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.
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.
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.