![]() |
Desktop Development »
Smart Client »
General
Intermediate
License: The Code Project Open License (CPOL)
GeoPlaces : hybrid smart client, involving RESTful WCF/WPF and moreBy Sacha BarberA nice explar of how to use RESTful WCF and WPF |
C# (C# 3.0), .NET (.NET 3.5), WCF, WPF, Architect, Dev, Design
|
||||||||||||
|
Advanced Search Add to IE Search |
|
|
||||||||||||||||||||
The last article I wrote was Sonic, which at the time of publishing, I stated was my best article yet. Now Sonic was pretty neat and used lots of nice stuff, but I have to say, that now I have finished this article, I think I am actually more happy with this one. Anyway you guys/girls can decide. I guess we will start by stating what this article actually does/doesn't do....so let's do that.
In one sense this article is probably the most useless (yes useless, you read right) article, in that it does not solve ANY specific problem domain, nor will it get you up to speed in 1 hour or get you out of a hole when you really need a component that does some missing code magic. However, on another level it should serve as a pretty decent template of how to work with technologies X/Y and Z should you need to use them.
Technology X/Y and Z in this case will be WCF (or Dub CF as I like to call it), and the new .NET 3.5 RESTful WCF possibilities, and also my personal favourite WPF (Dub PF), and it also makes use of the ADO .NET entity framework for persistence.
So essentially this article is about WCF/RESTful WCF/WPF. Now in order to show you how to do all the things that I considered to be important when working with these technologies, I needed some sort of problem that was able to solved using these technologies, I know, I know I have already said there is no problem solved by this article, well there is a hyperthetical problem domain which is what this article solves. To this end I developed the attached demo code. So what does it all do, get on with it Sacha, stop blabbering and tell us what it all does. Ok well it is pretty simple, I have created the following:
Although the demo app problem domain is dead simple, it has enough meat to allow me to demonstrate most of things you will need to know in order to work with WPF/WCF/RESTful WCF and it also uses ADO .NET entity framework, which some readers may not have come up against before.
This article contains a lot of code sections, probably too many, but I just felt that they were all quite neseccary to demonstrate the text a little better. So I am sorry if you are bit overwhelmed by it all.
Right so before we proceed let me just show you what it looks like. Essentially there are 3 steps through the app. The user may not advance past step 1 unless they complete the login or registration process.
STEP 1 : Login Or Register
If logging in, the user must type in their pre-saved username and password (the details they registered with) or they can register as a new user. You can see that there are also 2 arrow like buttons or the left and right which allow you to advance/go back to previous steps.
NOTE : The articles code includes a SQL script to set up a user and some places of interest to allow you to start with some dummy data. I will discuss this in more detail within the Things To Do To Get It Running For You section

STEP 2 : Create A New Place / Look At Your Saved Places
Once a user has been logged in, the STEP navigation buttons will be enabled, which will allow the user to skip to step2. The UI itself will perform a nice smooth animation to get to step 2. It looks nice, try it out for yourself.
When the user is at step 2, they can enter new details about geographical places of interest to them. To do this the user will need to enter the following information:
The entering of these details is via a control that is hosted in a 3D flippable surface, so that the user can enter a new place and flip the 3D surface and see all the places that they have managed to save (persist to SQL via ADO .NET entity framewowrk model)
Here is what the user sees at step 2 by default

And here it is 1/2 way though a 3D flip

And here are all the places after a flip (shown on the back of the 3D mesh)

STEP 3: Allows Users To Look At Saved Places Using Virtual Earth
The last step allows users to view the saved places on the hosted Virtual Earth instance. Obviously if there are no places saved for the current user, there is not going to be any items to show.
Assuming that there are places to show, the user can select one and the hosted VE instance will show it, and do a fly to zoom, to the place that the user selected. The hosted VE instance can also be controlled in the following ways from the WPF Client :
Here is an example showing one of my favourite places in the entire world, "The Chrysler Building" in NY City, firstly the user clicks on the saved place

Then we get automatically zoomed to it.

So that is what it all looks like. Some of you may not like the visual style, but doing an article is a personal thing, and this is the style I like, so I do it the way I like, cos it is my article, simple as that.
As I mentioned early on within the text of this article, this article makes use of the following technologies:
As such there is obviously a list of prerequistites that need to be covered if you wish to run this articles code at home/work. This list is as follows:
D3DImage for the DirectX interop used
for the hosted VE instance)
In order to get the code up and running you will need to download and have installed all the prerequisites above. Having done that, you should do the following in the order shown below:
1. Creat Empty SQL Database
Within your own SQL installation, create a new database called "GeoPlaces".
2. Setup SQL Database
The articles code includes a SQL script that can be used to setup the actual SQL server database schema. This SQL script is called "GenerateDBScript.sql". This script MUST be run against an existing SQL database called "GeoPlaces". This script simply adds 2 tables to the "GeoPlaces" database, these 2 tables will be "Users" and "Places".
3. Setup SQL Dummy Data
The articles code includes a SQL script that can be used to setup some initial dummy SQL data. This SQL script is called "CreateInitialDummyData.sql". This SQL script simply adds a single user called "sacha" with a password of "sacha" with a couple of my favourite places, so the hosted VE instance will have some places to show, should you log in as "sacha" with a password of "sacha".
4. Change the App.Config within the GeoPlacesServiceHost project
The articles demo code includes a solution with a project called GeoPlacesServiceHost, you should now ammend the App.Config to point to your own SQL server installation. That would be these lines:
<!-- HOME-->
<connectionStrings>
<add name="GeoPlacesEntities"
connectionString="metadata=res://GeoPlacesData/GeoPlacesModel.csdl|res://GeoPlacesData/GeoPlacesModel.ssdl|
res://GeoPlacesData/GeoPlacesModel.msl;provider=System.Data.SqlClient;provider
connection string="Data Source=YOUR SQL NAME HERE;Initial Catalog=GeoPlaces;User ID=sa;Password=sa;
MultipleActiveResultSets=True"" providerName="System.Data.EntityClient" />
</connectionStrings>
The part that you will need to change is the ADO .NET entity framework connection string. This should be simply a matter of changing the DataSource to point to your SQL server instance.
5. Start/Install The Service Host
In order to run a WCF Service of any kind (RESTful or SOAP based), the WCF service must be hosted somewhere. There are various options for this:
The articles demo code is written to support either a self hosting console app that will host the RESTful WCF service when run, or is able to install an actual Windows service that hosts the RESTful WCF service.
I can not tell you what option to go for, that is up to you. Rather I will talk you through both hosting options that I allow for, and you can decide what you want to use. Of course you must use and have running, one of these hosting options if you expect to be able to call the RESTful WCF service at all from the WPF client.
I have developed a simple class that caters for hosting the WCF service as either a Self Hosting Console App OR within a Windows service. The basic idea is that the current build mode (debug|release) is examined, and if it is debug, I will start the RESTful WCF hosting inside a console application with a infinite timeout. If the current build mode is release, I will attempt to use an actual Windows service host for the hosting.
The skeleton code that does this is shown below:
using System;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using System.Text;
using System.ServiceProcess;
namespace GeoPlacesServiceHost
{
static class Program
{
/// <summary>
/// The main entry point for the application.
/// Runs as console app if in debug.
/// </summary>
static void Main()
{
#if (!DEBUG)
try
{
Console.WriteLine(String.Format(
"Initialising GeoPlacesDataService.GeoService in assembly " +
"{0} RELEASE windows service mode.",
Assembly.GetExecutingAssembly().FullName));
ServiceBase[] ServicesToRun;
ServicesToRun = new ServiceBase[] { new Service() };
ServiceBase.Run(ServicesToRun);
}
catch (Exception ex)
{
Console.WriteLine(String.Format("Exception Occurred :", ex.Message));
}
#else
try
{
Console.WriteLine(String.Format(
"Initialising GeoPlacesDataService.GeoService in assembly {0} DEBUG console mode.",
Assembly.GetExecutingAssembly().FullName));
Console.WriteLine("Starting GeoPlacesDataService.GeoService");
Service.StartService();
Console.WriteLine("GeoPlacesDataService.GeoService Started");
System.Threading.Thread.Sleep(System.Threading.Timeout.Infinite);
}
catch (Exception ex)
{
Console.WriteLine(String.Format("Exception Occurred :", ex.Message));
}
#endif
}
}
}
Where the actual Windows service class looks like this (should you be interested)
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Configuration;
using System.Data;
using System.Diagnostics;
using System.Linq;
using System.Reflection;
using System.ServiceModel;
using System.ServiceModel.Description;
using System.ServiceModel.Web;
using System.ServiceProcess;
using System.Text;
using GeoPlacesDataService;
namespace GeoPlacesServiceHost
{
/// <summary>
/// Windows service to host the actual Restful
/// WCF KMLService
/// </summary>
public partial class Service : ServiceBase
{
#region Data
private static ServiceHost GeoPlacesServiceHost;
#endregion
#region Ctor
public Service()
{
InitializeComponent();
}
#endregion
#region Overrides
protected override void OnStart(string[] args)
{
Service.StartService();
}
protected override void OnStop()
{
try
{
Service.StopServiceHost(GeoPlacesServiceHost);
}
catch (Exception ex)
{
Console.WriteLine(String.Format(
"Exception while attempting to stop GeoService " +
"service type {0} the following exception was thrown {1}.",
this.GetType().FullName, ex.ToString()));
}
}
#endregion
#region Public Methods
public static void StartService()
{
try
{
GeoPlacesServiceHost = new WebServiceHost(typeof(GeoService),
new Uri(ConfigurationManager.AppSettings[
"GeoServiceEndpointAddress"]));
StartServiceHost(GeoPlacesServiceHost);
}
catch (TargetInvocationException tiEx)
{
Console.WriteLine(String.Format("Exception occurred", tiEx.Message));
}
catch (Exception ex)
{
Console.WriteLine(String.Format("Exception occurred", ex.Message));
}
}
#endregion
#region Private Methods
private static void StartServiceHost(ServiceHost serviceHost)
{
Boolean openSucceeded = false;
try
{
serviceHost = new WebServiceHost(typeof(GeoService),
new Uri(ConfigurationManager.AppSettings[
"GeoServiceEndpointAddress"]));
serviceHost.Open();
openSucceeded = true;
}
catch (Exception ex)
{
Console.WriteLine(String.Format(
"A failure occurred trying to open the " +
"GeoService ServiceHost, Error message : {0}",
ex.Message));
}
finally
{
if (!openSucceeded)
{
serviceHost.Abort();
Console.WriteLine(String.Format("{0} Aborted.",
serviceHost.Description.Name));
}
}
if (serviceHost.State == CommunicationState.Opened)
{
serviceHost.Faulted += ServiceHost_Faulted;
Console.WriteLine("GeoService is running...");
}
else
{
Console.WriteLine("GeoService failed to open");
Boolean closeSucceeded = false;
try
{
serviceHost.Close();
closeSucceeded = true;
Console.WriteLine(String.Format("{0} Closed.",
serviceHost.Description.Name));
}
catch (Exception ex)
{
Console.WriteLine(String.Format(
"A failure occurred trying to close the " +
"GeoServicee ServiceHost, Error message : {0}",
ex.Message));
}
finally
{
if (!closeSucceeded)
{
serviceHost.Abort();
Console.WriteLine(String.Format("{0} Aborted.",
serviceHost.Description.Name));
}
}
}
}
private static void StopServiceHost(ServiceHostBase serviceHost)
{
if (serviceHost.State != CommunicationState.Closed)
{
Boolean closeSucceeded = false;
try
{
serviceHost.Close();
closeSucceeded = true;
Console.WriteLine(String.Format("{0} Closed.",
serviceHost.Description.Name));
}
catch (Exception ex)
{
Console.WriteLine(String.Format(
"A failure occurred trying to close the " +
"GeoService ServiceHost, Error message : {0}",
ex.Message));
}
finally
{
if (!closeSucceeded)
{
serviceHost.Abort();
Console.WriteLine(String.Format("{0} Aborted.",
serviceHost.Description.Name));
}
}
}
}
private static void RestartServiceHost(ServiceHost serviceHost)
{
StopServiceHost(serviceHost);
StartServiceHost(serviceHost);
}
private static void LogServiceHostInfo(ServiceHostBase serviceHost)
{
var strBuilder = new StringBuilder();
strBuilder.AppendFormat("'{0}' Starting", serviceHost.Description.Name);
strBuilder.Append(Environment.NewLine);
// Behaviors
var annotation =
serviceHost.Description.Behaviors.Find<ServiceBehaviorAttribute>();
strBuilder.AppendFormat("Concurrency Mode = {0}",
annotation.ConcurrencyMode);
strBuilder.Append(Environment.NewLine);
strBuilder.AppendFormat("InstanceContext Mode = {0}",
annotation.InstanceContextMode);
strBuilder.Append(Environment.NewLine);
// Endpoints
strBuilder.Append("The following endpoints are exposed:");
strBuilder.Append(Environment.NewLine);
foreach (ServiceEndpoint endPoint in serviceHost.Description.Endpoints)
{
strBuilder.AppendFormat("{0} at {1} with {2} binding; "
, endPoint.Contract.ContractType.Name
, endPoint.Address
, endPoint.Binding.Name);
strBuilder.Append(Environment.NewLine);
}
// Metadata
var metabehaviour =
serviceHost.Description.Behaviors.Find<ServiceMetadataBehavior>();
if (metabehaviour != null)
{
if (metabehaviour.HttpGetEnabled)
{
if (metabehaviour.HttpsGetUrl != null)
{
strBuilder.AppendFormat("Metadata enabled at {0}",
serviceHost.BaseAddresses[0]);
}
else
{
strBuilder.AppendFormat("Metadata enabled at {0}",
metabehaviour.HttpGetUrl);
}
}
if (metabehaviour.HttpsGetEnabled)
strBuilder.AppendFormat(" and {0}.", metabehaviour.HttpsGetUrl);
if (metabehaviour.ExternalMetadataLocation != null)
strBuilder.AppendFormat(" Metadata can be found externally at {0}",
metabehaviour.ExternalMetadataLocation);
}
Console.WriteLine(strBuilder.ToString());
}
private static void ServiceHost_Faulted(Object sender, EventArgs e)
{
var serviceHost = sender as ServiceHost;
Console.Write(String.Format("{0} Faulted. Attempting Restart.",
serviceHost.Description.Name));
RestartServiceHost(serviceHost);
}
#endregion
}
}
There is also a Installer for the Windows service, but that is pretty bulk standard stuff so I will not muddy the waters with that.
Anyway I just wanted to explain the methods of hosting, now let's discuss how you can use these different hosting methods:
Self Hosting Console App
This is probably the easiest way to host WCF services, as it is a simple class with a main method and a bit of code to host the service and really that is it. All you have to do to use this method, is navigate to the GeoPlacesServiceHost\bin\Debug\ folder and run the GeoPlacesServiceHost.exe console app.
You should then see the host started as follows:

Windows Service Hosting
Like I say I have created a class that allows console hosting in debug mode and Windows service hosting in release mode. So in order to host the WCF service within a Windows service we need to carry out the following steps:
Install the service.
so from the command line run the following command line, installutil.exe "XXXXXXXXX\GeoPlacesServiceHost\bin\Release\GeoPlacesServiceHost.exe", where XXXXXXXXX is your path to the GeoPlaces code you have downloaded.
This will present a login screen that you need to fill in for the service to run.

This will place a new Windows service within the list of available Windows services. After this installation, you will need to start the Windows service. Go to the list of available Windows services and start the GeoPlaces service.

I tend to prefer the Console app method, when I am working in Visual Studio developing code, and the Windows service method for actually finished deployable code. I think Windows service hosting is cool, as the actual Windows service can be made to manage starting / stopping of the hosted RESTful WCF service. Which is cool, as it is something I no longer have to worry about, basically the Windows service manages the hsoted RESTful WCF service lifecycle.
WCF has been around a while now and there are lots of articles on using it, so I will not cover the basics (if you like you can read one my older WCF articles to learn the basics about SOAP based WCF), rather I am going to talk about how to work with the new RESTful WCF capabilities.
Fo those that do not even know what REST is, REST stands for Representational state transfer (REST). The term is often used more loosely to describe any simple interface which transmits domain-specific data over HTTP without an additional messaging layer such as SOAP or session tracking via HTTP cookies.
When working with RESTful WCF what we are really talking about is exposing Type(s) as either JSON or XML at a specific Uri.
As some of you may know standard SOAP based WCF is exposed via endpoints, and RESTful WCF is no different, the only difference is that as well as proxy classes to talk to the RESTful WCF service the user may use a standard browser.
This is done by the use of the standard HTTP Verbs
So how does all this work, well it is actually quite simple, part of .NET 3.5
allows us to adorn our WCF service OperationContract(s) with additional
RESTful attributes. Let us examine a few of these.
Within the GeoPlaces database (if you have populated it with the sample data) there will be a number of places stored against a specific user (for me this user is 10, it may be different for you) so we have a table that looks like this in SQL server

We also have a RESTful WCF service method that looks like this
[OperationContract]
[WebGet(UriTemplate = "/placesList/{userId}",
ResponseFormat = WebMessageFormat.Xml)]
List<Places> GetAllPlacesForUser(String userId);
Note the use of the WebGetAttribute, which has specified a UriTemplate,
this allows the user to type a Uri into a browser that will pattern match against
all the methods within the RESTful WCF service to see if there is a match. If
there is a match that method will be called, and the return value will be serialized
to the format the user specifies, XML in this case.
This is what the actual method implemention looks like
/// <summary>
/// Gets all places for a particular user
/// </summary>
public List<Places> GetAllPlacesForUser(String userId)
{
Int32 id = -1;
if (Int32.TryParse(userId, out id))
{
try
{
GeoPlacesEntities model = new GeoPlacesEntities();
return model.Places.Where((pl) => pl.Users.ID == id).ToList();
}
catch (Exception ex)
{
//WebProtocolException is part of WCF REST Starter Kit Preview 2
throw new WebProtocolException(HttpStatusCode.BadRequest,
String.Format("Couldn't find places for user id {0}", userId), null);
}
}
else
return null;
}
So how should a user be able to call a RESTful service method using a GET.
Well it is still down to the WCF service being hosted somewhere. In the attached
demo code the WCF service can either be hosted within a Console app or within
a Windows service, in both cases, there is a App.Config setting that dictates
that the RESTful service should be available at the following address "http://localhost:8085/GeoPlacesDataService".
So if the RESTful WCF service is hosted and running, the user is then able to
type the following, which matches the WebGetAttribute UriTemplate
match we saw above
http://localhost:8085/GeoPlacesDataService + /placesList/{userId} with actual substitutions into a browser to obtain resource. An example may be
http://localhost:8085/GeoPlacesDataService/placesList/10, which we would expect to return a list of place resources for the user who has an Id of 10. Lets try that in the browser.
NOTE : The method parameters MUST match the substitution sections within the
UriTemplate, and all parameters must be of type String, as this
is all that is possible through a browser.

What do you know it works, this is all thanks to the pattern matching and the
WebGetAttribute, which has specified a UriTemplate,
that allows the pattern matching to work, and find the correct method to call.
You can see that this is XML, and you may be wondering where this XAML comes
from. Well the demo code is using a ADO .NET entity framework to persist data
to/from SQL server, so this is the default serialization that occurs for the
Places object within the demo codes ADO .NET entity framework Model.
To fully undestand what is happening here I will show you what the Places
class looks like.
/// <summary>
/// There are no comments for GeoPlacesModel.Places in the schema.
/// </summary>
/// <KeyProperties>
/// ID
/// </KeyProperties>
[global::System.Data.Objects.DataClasses.EdmEntityTypeAttribute(
NamespaceName="GeoPlacesModel", Name="Places")]
[global::System.Runtime.Serialization.DataContractAttribute(IsReference=true)]
[global::System.Serializable()]
public partial class Places : global::System.Data.Objects.DataClasses.EntityObject
{
/// <summary>
/// Create a new Places object.
/// </summary>
/// <param name="ID">Initial value of ID.</param>
/// <param name="name">Initial value of Name.</param>
/// <param name="description">Initial value of Description.</param>
/// <param name="longitude">Initial value of Longitude.</param>
/// <param name="latitude">Initial value of Latitude.</param>
public static Places CreatePlaces(int ID, string name,
string description, double longitude, double latitude)
{
Places places = new Places();
places.ID = ID;
places.Name = name;
places.Description = description;
places.Longitude = longitude;
places.Latitude = latitude;
return places;
}
/// <summary>
/// There are no comments for Property ID in the schema.
/// </summary>
[global::System.Data.Objects.DataClasses.EdmScalarPropertyAttribute(
EntityKeyProperty=true, IsNullable=false)]
[global::System.Runtime.Serialization.DataMemberAttribute()]
public int ID
{
get
{
return this._ID;
}
set
{
this.OnIDChanging(value);
this.ReportPropertyChanging("ID");
this._ID = global::System.Data.Objects.DataClasses.
StructuralObject.SetValidValue(value);
this.ReportPropertyChanged("ID");
this.OnIDChanged();
}
}
private int _ID;
partial void OnIDChanging(int value);
partial void OnIDChanged();
/// <summary>
/// There are no comments for Property Name in the schema.
/// </summary>
[global::System.Data.Objects.DataClasses.EdmScalarPropertyAttribute(IsNullable=false)]
[global::System.Runtime.Serialization.DataMemberAttribute()]
public string Name
{
get
{
return this._Name;
}
set
{
this.OnNameChanging(value);
this.ReportPropertyChanging("Name");
this._Name = global::System.Data.Objects.DataClasses.
StructuralObject.SetValidValue(value, false);
this.ReportPropertyChanged("Name");
this.OnNameChanged();
}
}
private string _Name;
partial void OnNameChanging(string value);
partial void OnNameChanged();
/// <summary>
/// There are no comments for Property Description in the schema.
/// </summary>
[global::System.Data.Objects.DataClasses.EdmScalarPropertyAttribute(IsNullable=false)]
[global::System.Runtime.Serialization.DataMemberAttribute()]
public string Description
{
get
{
return this._Description;
}
set
{
this.OnDescriptionChanging(value);
this.ReportPropertyChanging("Description");
this._Description = global::System.Data.Objects.DataClasses.
StructuralObject.SetValidValue(value, false);
this.ReportPropertyChanged("Description");
this.OnDescriptionChanged();
}
}
private string _Description;
partial void OnDescriptionChanging(string value);
partial void OnDescriptionChanged();
/// <summary>
/// There are no comments for Property Longitude in the schema.
/// </summary>
[global::System.Data.Objects.DataClasses.
EdmScalarPropertyAttribute(IsNullable=false)]
[global::System.Runtime.Serialization.DataMemberAttribute()]
public double Longitude
{
get
{
return this._Longitude;
}
set
{
this.OnLongitudeChanging(value);
this.ReportPropertyChanging("Longitude");
this._Longitude = global::System.Data.Objects.DataClasses.
StructuralObject.SetValidValue(value);
this.ReportPropertyChanged("Longitude");
this.OnLongitudeChanged();
}
}
private double _Longitude;
partial void OnLongitudeChanging(double value);
partial void OnLongitudeChanged();
/// <summary>
/// There are no comments for Property Latitude in the schema.
/// </summary>
[global::System.Data.Objects.DataClasses.EdmScalarPropertyAttribute(IsNullable=false)]
[global::System.Runtime.Serialization.DataMemberAttribute()]
public double Latitude
{
get
{
return this._Latitude;
}
set
{
this.OnLatitudeChanging(value);
this.ReportPropertyChanging("Latitude");
this._Latitude = global::System.Data.Objects.DataClasses.
StructuralObject.SetValidValue(value);
this.ReportPropertyChanged("Latitude");
this.OnLatitudeChanged();
}
}
private double _Latitude;
partial void OnLatitudeChanging(double value);
partial void OnLatitudeChanged();
You can see that it implements Serializable and also has a DataContractAttribute
which will use the DataContractSerializer for serialization.You can see it simply
gets serialized and sent as XML (not very pretty XML but it does work just fine).
If you want more control over the way the resources are serialized, you can
write you own serialization using a return type of Message. I talk
a lot more about serialization options when working with RESTful WCF, over at
my blog, you can read about your options using this post http://sachabarber.net/?p=475
Ok so that is the basic idea, we have some new RESTful attributes, and we can expose the service methods, via a HTTP Uri, and we can pattern match on certain Uris and even use part of the Uri as parameters to methods.
Now I am all in favour of using a browser when the entire service consists
of GET requests, as there is no need for a DataContract client
proxy class at all. The browser is able to get the resource(s) it wants by simply
using a specific Uri. However as we all know things are never as clear cut as
all that, and most, if not all applications will be required to update/delete
and alter data, to this end you will almost always need to use some of the other
HTTP verbs apart from GET requests.
These HTTP verbs are more than likely going to need entire objects to be sent, where these object are either XML / JSON serlialized and which are going to be needed to be deserialized and translated into CLR types. Now I like coding, but I am no masichist and I would always choose to pick the right tool for the right job, to this end I would recommend using a proxy, which I discuss in detail below in the WPF Client section.
For now all you need to know is that there is WebInvokeAttribute
which can be used for the POST/PUT and DELETE http verbs.
Here is an example.
[OperationContract]
[WebInvoke(Method = "POST", UriTemplate = "User/Add/",
ResponseFormat = WebMessageFormat.Xml)]
Users AddUser(Users newUser);
The WebInvokeAttribute MUST be specified with
a Method type as well. Possible values are POST/PUT and DELETE,
this is required as all 3 http verbs may end up with the same Uri template and
there needs to be some way to distinguish between them.
Those of you that have worked with WCF before may have come across something
called SOAP faults, which are realized using FaultException<T>
in normal SOAP based WCF services. FaultException<T> allow
you to flow SOAP faults back to the client app of the WCF service. Now as we
are working with http with a RESTful service, we can no longer use FaultException<T>
as we need a http representive fault message. As luck would have it, Microsoft
have thoght about this and released a RESTful WCF starter kit out of bands,
which contains a number of useful classes. One of which is the WebProtocolException
class which allows WebProtocolExceptions to be treated just like
normal WebProtocolExceptions. These can then be used by the client
of the RESTful WCF service. Here is an example of the actual RESTful WCF service
raising a WebProtocolException :
/// <summary>
/// Gets all places for a particular user
/// </summary>
public List<Places> GetAllPlacesForUser(String userId)
{
Int32 id = -1;
if (Int32.TryParse(userId, out id))
{
try
{
GeoPlacesEntities model = new GeoPlacesEntities();
return model.Places.Where((pl) => pl.Users.ID == id).ToList();
}
catch (Exception ex)
{
//WebProtocolException is part of WCF REST Starter Kit Preview 2
throw new WebProtocolException(HttpStatusCode.BadRequest,
String.Format("Couldn't find places for user id {0}", userId), null);
}
}
else
return null;
}
Note that with SOAP based WCF using FaultException<T> meant
that we had to mark up the ServiceContract with FaultContractAttribute(s),
this is not required using the WebProtocolException class.
Wikipedia has this to say about ETags
"An ETag (entity tag) is an HTTP response header returned by an HTTP/1.1 compliant web server used to determine change in content at a given URL. When a new HTTP response contains the same ETag as an older HTTP response, the contents are considered to be the same without further downloading. The header is useful for intermediary devices that perform caching, as well as for client web browsers that cache results. "
However Jon Flanders who is the author of an excellent book on rest says it better in my opinion. What he says is this
"An ETag is a per resource, opaque, unique value. An ETag is generally a hashed value generated by a server in response to a GET request for a resource that is based on some information from tne resource itself. When the user agent makes another request for the same resource, the value of the ETag is presented in the If-None-Match header.
When the server receives the request, it has to generate the ETag for the resource again, and if the current ETag matches the value of the If-No-Match header, the resource hasn't changed."
Jon Flanders, RESTful .NET, O'Reilly.
Now what all this means to developers of restful WCF services is that when we get a request for a resource we should be good developers and create an ETag that can be checked the next time a request is made.
Consider the GET method that the demo code has
[OperationContract]
[WebGet(UriTemplate = "/users/{userId}",
ResponseFormat = WebMessageFormat.Xml)]
Users GetUser(String userId);
Which has an actual implementation as follows:
/// <summary>
/// Gets a user based on its Id
/// </summary>
public Users GetUser(String userId)
{
Int32 id = -1;
if (Int32.TryParse(userId, out id))
{
try
{
Users u = FindUser(id);
#if HTTP
string etag = GenerateETag(u.ID + u.Name + u.Password);
if (CheckETag(etag))
return null;
if (u == null)
{
OutgoingWebResponseContext ctx =
WebOperationContext.Current.OutgoingResponse;
ctx.SetStatusAsNotFound();
ctx.SuppressEntityBody = true;
}
SetETag(etag);
#endif
return u;
}
catch (Exception ex)
{
//WebProtocolException is part of WCF REST Starter Kit Preview 2
throw new WebProtocolException(HttpStatusCode.BadRequest,
String.Format("Couldn't find user with id {0}", userId), null);
}
}
else
return null;
}
You will notice that this code uses 3 ETag helper methods (if #HTTP is defined) which are listed below, whcih are used to manage the checking of ETags and creation of new ETags. This allows caching of resources :
/// <summary>
/// Sets a ETag (caching for the object) on the current
/// OutgoingResponse context
/// </summary>
private void SetETag(string etag)
{
OutgoingWebResponseContext ctx =
WebOperationContext.Current.OutgoingResponse;
ctx.ETag = etag;
}
/// <summary>
/// Creates a ETag (caching for the object)
/// </summary>
private string GenerateETag(String valueToHash)
{
byte[] bytes = Encoding.UTF8.GetBytes(valueToHash);
byte[] hash = MD5.Create().ComputeHash(bytes);
string etag = Convert.ToBase64String(hash);
return etag;
}
/// <summary>
/// Examines the incoming request context to see if there
/// is a cached ETag for the object requested
/// </summary>
private bool CheckETag(string currentETag)
{
IncomingWebRequestContext ctx =
WebOperationContext.Current.IncomingRequest;
string incomingEtag =
ctx.Headers[HttpRequestHeader.IfNoneMatch];
if (incomingEtag != null)
{
if (currentETag == incomingEtag)
{
return true;
}
}
return false;
}
As users of a RESTful WCF service may add new resources, they should be able to get to these added resources using a Uri, so it is plain polite that whenever a new resource is added that you (the developer) create a new Uri for the new resource. This can be seen when we allow a new Users to be added.
[OperationContract]
[WebInvoke(Method = "POST", UriTemplate = "User/Add/",
ResponseFormat = WebMessageFormat.Xml)]
Users AddUser(Users newUser);
Where a Users resource is available using the following
[OperationContract]
[WebGet(UriTemplate = "/users/{userId}",
ResponseFormat = WebMessageFormat.Xml)]
Users GetUser(String userId);
So if we examine the implementation of the AddUser() method we can see that when a new Users resource is created, we also play nice and create a new Uri for the new Users resource. This will be returned via the current web context, and the user will then be able to use the new Uri to GET the newly added resource.
///
/// Adds a user to the System
///
public Users AddUser(Users newUser)
{
try
{
GeoPlacesEntities model = new GeoPlacesEntities();
model.AddToUsers(newUser);
model.SaveChanges();
Users userFromDb = model.Users.Where((u) =>
u.Name.ToLower().Equals(newUser.Name) &&
u.Password.ToLower().Equals(newUser.Password)
).First();
#if HTTP
OutgoingWebResponseContext ctx =
WebOperationContext.Current.OutgoingResponse;
ctx.SetStatusAsCreated(CreateNewUserUri(userFromDb));
#endif
return userFromDb;
}
catch(Exception ex)
{
//WebProtocolException is part of WCF REST Starter Kit Preview 2
throw new WebProtocolException(HttpStatusCode.BadRequest,
"Couldn't add new user", null);
}
}
///
/// Create a new User Uri for the Resource
///
private Uri CreateNewUserUri(Users u)
{
UriTemplate ut = new UriTemplate("/users/{user_id}");
Uri baseUri = WebOperationContext.Current.
IncomingRequest.UriTemplateMatch.BaseUri;
Uri ret = ut.BindByPosition(baseUri, u.ID.ToString());
return ret;
}
The last thing I wanted to talk about is WCF extensability. Basically if you can think of any thing you want to extend in WCF, there will be an extension point. For example I wanted all my http Content-Type header values to be set automatically without me having to write this for every request/response.
As it turns out you can do this by extending the IEndpointBehavior,
as shown below.
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.ServiceModel.Description;
using System.ServiceModel.Channels;
using System.ServiceModel.Dispatcher;
namespace GeoPlacesDataService
{
/// <summary>
/// Taken directly from the excellent RESTful .NET by Jon Flanders
///
/// It simply alters the endpoint behaviour by automatically setting the
/// Content Type, by the use of the ContentTypeMessageInspector class
/// </summary>
public class ContentTypeBehaviour : IEndpointBehavior
{
public string ContentType { get; set; }
#region IEndpointBehavior Members
public void AddBindingParameters(ServiceEndpoint endpoint,
BindingParameterCollection bindingParameters)
{
//do nothing
}
public void ApplyClientBehavior(ServiceEndpoint endpoint,
ClientRuntime clientRuntime)
{
//work out what the correct ContentType should be
ContentTypeMessageInspector mi = new
ContentTypeMessageInspector { ContentType = this.ContentType };
clientRuntime.MessageInspectors.Add(mi);
}
public void ApplyDispatchBehavior(ServiceEndpoint endpoint,
EndpointDispatcher endpointDispatcher)
{
//do nothing
}
public void Validate(ServiceEndpoint endpoint)
{
//do nothing
}
#endregion
}
}
Where this class makes use of a helper class called ContentTypeMessageInspector
which is as shown below:
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.ServiceModel.Dispatcher;
using System.ServiceModel.Channels;
using System.ServiceModel;
namespace GeoPlacesDataService
{
/// <summary>
/// Taken directly from the excellent RESTful .NET by Jon Flanders
///
/// Automatically sets the Content Type, by extending WCF namely
/// setting the HttpRequestMessageProperty headers
/// </summary>
public class ContentTypeMessageInspector : IClientMessageInspector
{
public string ContentType { get; set; }
#region IClientMessageInspector Members
public void AfterReceiveReply(ref Message reply, object correlationState)
{
//do nothing
}
/// <summary>
/// Apply the Content-Type header to the HttpRequestMessageProperty
/// </summary>
public object BeforeSendRequest(ref Message request,
IClientChannel channel)
{
HttpRequestMessageProperty prop =
request.Properties[HttpRequestMessageProperty.Name]
as HttpRequestMessageProperty;
if (prop != null && (prop.Method == "POST" || prop.Method == "PUT"))
{
prop.Headers["Content-Type"] = this.ContentType;
}
return null;
}
#endregion
}
}
I makes no claims to have authored these 2 classes myself, these are lifted directly from Jon Flanders excellent RESTful .NET book. Jon knows his stuff, its a great book, highly recommended.
There is a WPF Client app that makes use of the running RESTful WCF service that is hosted within a Windows Service (Or self hosted in a Console app, if you simply use the standalon EXE) this section will outline the most important parts of the WPF Client.
If you are working with WPF you really should be using the MVVM pattern, which
allows the view to be abstracted to a ViewModel. The idea being that the ViewModel
can be tested in isolation and that the View code simply contains the actual
presentation, this being the XAML. Typically the View will bind to an associated
ViewModels properties, where the ViewModel which will be set as the Views DataContext.
The ViewModel will either expose bindable properties as DependencyProperty
or CLR Properties where INotifyPropertyChanged will be used to
ensure change notification (allows bindings to update to latest values).
The attached demo code has several View and ViewModels all performing various parts of the overall functionality of the WPF Client, but if you were to examine the code behind for the View(s) you will see very little code. The ViewModel is doing all the work. Here is a list of the Views/ViewModels.
| View Name | View Model | Description |
| LoginControl | LoginViewModel | Allows user to login or register |
| MainWindow | MainWindowViewModel | The main window |
| PlaceControl | PlacesViewModel | Respresents the places for a user |
Let's consider a single example, say the Login View/ViewModel which I will be using for most of the discussions in subsequent sections:
Here is the ViewModel code (NOTE: ViewModelBase implements the INotifyPropertyChanged
interface).
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Linq;
using System.Text;
using System.Windows.Input;
using GeoPlacesModel;
namespace GeoPlaces
{
/// <summary>
/// Simple ViewModel for the LoginControl
/// </summary>
public class LoginViewModel : ViewModelBase
{
#region Data
private String userName = String.Empty;
private String password = String.Empty;
private Boolean isAuthenticatedUser = false;
private Boolean isBusy = false;
private ICommand loginCommand = null;
private ICommand registerCommand = null;
private IView view = null;
#endregion
#region Ctor
public LoginViewModel(IView view)
{
this.view = view;
//wire up loginCommand
loginCommand = new SimpleCommand
{
CanExecuteDelegate = x => !IsBusy,
ExecuteDelegate = x => Login()
};
//wire up registerCommand
registerCommand = new SimpleCommand
{
CanExecuteDelegate = x => !IsBusy,
ExecuteDelegate = x => Register()
};
}
#endregion
#region Public Properties
public ICommand RegisterCommand
{
get { return registerCommand; }
}
public ICommand LoginCommand
{
get { return loginCommand; }
}
public Boolean IsBusy
{
get { return isBusy; }
set
{
isBusy = value;
NotifyChanged("IsBusy");
}
}
public String UserName
{
get { return userName; }
set
{
userName = value;
isAuthenticatedUser = false;
NotifyChanged("IsAuthenticatedUser");
NotifyChanged("UserName");
}
}
public String Password
{
get { return password; }
set
{
password = value;
isAuthenticatedUser = false;
NotifyChanged("IsAuthenticatedUser");
NotifyChanged("Password");
}
}
public Boolean IsAuthenticatedUser
{
get { return isAuthenticatedUser; }
set
{
isAuthenticatedUser = value;
NotifyChanged("IsAuthenticatedUser");
}
}
#endregion
#region Private Methods
private void Login()
{
isBusy = true;
App.Current.Properties.Remove("CurrentUser");
Users dbReadUser = ServiceCalls.LoginUser(userName, password);
if (dbReadUser != null)
{
App.Current.Properties.Add("CurrentUser", dbReadUser);
Mediator.Instance.NotifyColleagues(
ViewModelMessages.IsAuthenticatedUser, true);
view.ShowMessage(String.Format(
"Sucessfully logged in user {0}, please proceed to view/add your places",
userName));
}
else
{
App.Current.Properties.Add("CurrentUser", null);
Mediator.Instance.NotifyColleagues(
ViewModelMessages.IsAuthenticatedUser, false);
view.ShowMessage(String.Format(
"Could not log in user {0}, please try again",
userName));
}
isBusy = false;
}
private void Register()
{
isBusy = true;
App.Current.Properties.Remove("CurrentUser");
Users dbReadUser = ServiceCalls.RegisterUser(userName, password);
if (dbReadUser != null)
{
App.Current.Properties.Add("CurrentUser", dbReadUser);
Mediator.Instance.NotifyColleagues(
ViewModelMessages.IsAuthenticatedUser, true);
view.ShowMessage(String.Format(
"Sucessfully added user {0}, please proceed to view/add your places",
userName));
}
else
{
App.Current.Properties.Add("CurrentUser", null);
Mediator.Instance.NotifyColleagues(
ViewModelMessages.IsAuthenticatedUser, false);
view.ShowMessage(String.Format(
"Could not add user {0}, please try again",
userName));
}
isBusy = false;
}
#endregion
}
}
And here is the LoginControl.xaml
<StackPanel Orientation="Vertical">
<Label FontSize="18" Content="Login or Register"/>
<StackPanel Orientation="Horizontal">
<Label FontSize="14" Content="UserName" Width="100"/>
<TextBox Text="{Binding Path=UserName, Mode=TwoWay,
UpdateSourceTrigger=LostFocus}"/>
</StackPanel>
<StackPanel Orientation="Horizontal">
<Label FontSize="14" Content="Password" Width="100"/>
<TextBox Text="{Binding Path=Password, Mode=TwoWay,
UpdateSourceTrigger=LostFocus}"/>
</StackPanel>
<StackPanel Orientation="Horizontal">
<Button x:Name="btnLogin" Content="Login"
Height="30" Margin="10"
Style="{StaticResource GelButton}"
ToolTip="Login Using Your Previous Details"
Command="{Binding Path=LoginCommand}"/>
<Button x:Name="btnRegister" Content="Register"
Height="30" Margin="10"
ToolTip="Register As A New User"
Style="{StaticResource GelButton}"
Command="{Binding Path=RegisterCommand}"/>
</StackPanel>
</StackPanel>
The ViewModel is set as the DataContext in the code behind as follows. Notice how the code behind is pretty dumb, the ViewModel does the lions share of the work, the View is pretty passive and just responds to changes via binding updates from the ViewModel.
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Data;
using System.Windows.Documents;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Media.Imaging;
using System.Windows.Navigation;
using System.Windows.Shapes;
namespace GeoPlaces
{
/// <summary>
/// A simple Login control that allows existing
/// users to login, or new users to register
/// </summary>
public partial class LoginControl : UserControl, IView
{
#region Data
private LoginViewModel loginViewModel = null;
#endregion
#region Ctor
public LoginControl()
{
InitializeComponent();
this.Loaded += new RoutedEventHandler(LoginControl_Loaded);
loginViewModel = new LoginViewModel(this);
}
#endregion
#region Private Methods
private void LoginControl_Loaded(object sender, RoutedEventArgs e)
{
this.DataContext = loginViewModel;
}
#endregion
#region IView Members
public void ShowMessage(string message)
{
MessageBox.Show(message,"information",
MessageBoxButton.OK,MessageBoxImage.Information);
}
#endregion
}
}
In order to fully support the MVVM pattern you will want to consider some sort
of commanding infrastructure, such that the View can binding to an instance
of ICommand typically exposed as properties on the ViewModel, and
ideally would allow the commands code to be executed within the ViewModel. While
this is possible using the standard WPF RoutedCommands, it is a bit tedious
as it requires the developer to create command binding in the XAML or the code
behind for the View and wire up delegates etc etc, believe me, it's tedious.
Lately a number of people including those working on the Microsoft Composite
WPF and Silverlight (AKA PRISM) code, have abandoned the standard WPF commanding
in favour of lighter weight easier to use delegate based commands. The ViewModel
simply exposes an ICommand property that the View binds to. The
ViewModel contains the delegates that are run whe the ICommand
that the View is bound executes. The delegate based commands also enable the
command exeuction state to be determined and shown within the View. Here is
an example, where we make use of Predicate<object> for the
CanExecute handler from ICommand, and Action<object>
for the Execute delegate of ICommand.
ViewModel code
/// <summary>
/// Simple ViewModel for the LoginControl
/// </summary>
public class LoginViewModel : ViewModelBase
{
#region Data
....
....
private ICommand loginCommand = null;
private ICommand registerCommand = null;
#endregion
#region Ctor
public LoginViewModel(IView view)
{
this.view = view;
//wire up loginCommand
loginCommand = new SimpleCommand
{
CanExecuteDelegate = x => !IsBusy,
ExecuteDelegate = x => Login()
};
//wire up registerCommand
registerCommand = new SimpleCommand
{
CanExecuteDelegate = x => !IsBusy,
ExecuteDelegate = x => Register()
};
}
#endregion
#region Public Properties
public ICommand RegisterCommand
{
get { return registerCommand; }
}
public ICommand LoginCommand
{
get { return loginCommand; }
}
....
....
#endregion
}
View code
<StackPanel Orientation="Horizontal">
<Button x:Name="btnLogin" Content="Login"
Height="30" Margin="10"
Style="{StaticResource GelButton}"
ToolTip="Login Using Your Previous Details"
Command="{Binding Path=LoginCommand}"/>
<Button x:Name="btnRegister" Content="Register"
Height="30" Margin="10"
ToolTip="Register As A New User"
Style="{StaticResource GelButton}"
Command="{Binding Path=RegisterCommand}"/>
</StackPanel>
This uses my good buddy Marlon
Grechs SimpleCommand code which is as follows:
using System;
using System.Windows.Input;
namespace GeoPlaces
{
/// <summary>
/// Implements the ICommand and wraps up all the verbose
/// stuff so that you can just pass 2 delegates 1 for the
/// CanExecute and one for the Execute
/// </summary>
public class SimpleCommand : ICommand
{
/// <summary>
/// Gets or sets the Predicate to execute when the
/// CanExecute of the command gets called
/// </summary>
public Predicate<object> CanExecuteDelegate { get; set; }
/// <summary>
/// Gets or sets the action to be called when the
/// Execute method of the command gets called
/// </summary>
public Action<object> ExecuteDelegate { get; set; }
#region ICommand Members
/// <summary>
/// Checks if the command Execute method can run
/// </summary>
/// <param name="parameter">THe command parameter to
/// be passed</param>
/// <returns>Returns true if the command can execute.
/// By default true is returned so that if the user of
/// SimpleCommand does not specify a CanExecuteCommand
/// delegate the command still executes.</returns>
public bool CanExecute(object parameter)
{
if (CanExecuteDelegate != null)
return CanExecuteDelegate(parameter);
return true;// if there is no can execute default to true
}
public event EventHandler CanExecuteChanged
{
add { CommandManager.RequerySuggested += value; }
remove { CommandManager.RequerySuggested -= value; }
}
/// <summary>
/// Executes the actual command
/// </summary>
/// <param name="parameter">THe command parameter
/// to be passed</param>
public void Execute(object parameter)
{
if (ExecuteDelegate != null)
ExecuteDelegate(parameter);
}
#endregion
}
}
In the spirit of the MVVM pattern we do not want to polute our code behind with extra code that is obviously now done by the ViewModel code. The only problem is that occassionally the ViewModel needs to do something UI like, such as show a MessageBox. Believe me, you do want to try and stick with the ViewModel approach as it creates very clean seperated / easily testable code. However, this MessageBox situation just outlined is an issue, what can we do about it.
Well one approach is for the View to expose services such as, a MessageBox service would be one example. One of my fellow WPF Disciples Bill Kempf is working on this very idea in a great project called WPF Onyx, which I will be working on after this article. Onyx uses this service based approach to really help you create good WPF apps that use the MVVM pattern.
Anyway what I do for this demo app is simply pass the View into the ViewModel,
and from there the ViewModel is able to call a service off the View which is
exposed by known interfaces. Let's see an example, using the MessageBox problem
outlined above, notice the View implements an IView interface,
which exposes a MessageBox service method, that the ViewModel could call to
tell the View to do something, again this avoid spaghetti code in the code behind
of the View, as the ViewModel will only be able to communicate with the View
using well know exposes service interfaces.
VIEW Code
/// <summary>
/// A simple Login control that allows existing
/// users to login, or new users to register
/// </summary>
public partial class LoginControl : UserControl, IView
{
#region Data
private LoginViewModel loginViewModel = null;
#endregion
#region Ctor
public LoginControl()
{
InitializeComponent();
this.Loaded += new RoutedEventHandler(LoginControl_Loaded);
loginViewModel = new LoginViewModel(this);
}
#endregion
#region Private Methods
private void LoginControl_Loaded(object sender, RoutedEventArgs e)
{
this.DataContext = loginViewModel;
}
#endregion
#region IView Members
public void ShowMessage(string message)
{
MessageBox.Show(message,"information",
MessageBoxButton.OK,MessageBoxImage.Information);
}
#endregion
}
And here is the ViewModel code, notice how the constructor takes an IView
as a parameter, and when the ViewModel wants to show a MessageBox it asks it's
internal IView instance to do it, and it will use the MessageBox
service method we saw above. This way the ViewModel is able to perform UI type
things, but does not contain any UI code as such, the view does all that. The
ViewModel simply asks the view to do something via some well know interface
and the View does it. This concept is very powerful and enables extremely clean
code seperation between the presentation layer and the actual Model that drives
the View. I like this idea a lot, and can't wait to start trying a big article
with
WPF Disciples Bill
Kempf pet project
WPF Onyx you will definately see more from me on
WPF Onyx it looks very very promising.
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Linq;
using System.Text;
using System.Windows.Input;
using GeoPlacesModel;
namespace GeoPlaces
{
/// <summary>
/// Simple ViewModel for the LoginControl
/// </summary>
public class LoginViewModel : ViewModelBase
{
....
....
....
private IView view = null;
#region Ctor
public LoginViewModel(IView view)
{
this.view = view;
}
#endregion
#region Private Methods
private void Login()
{
.......
.......
view.ShowMessage(String.Format(
"Could not log in user {0}, please try again",
userName));
.......
.......
}
#endregion
}
}
As one can imagine if a single View is abstracted to a single ViewModel there will be occasions when View/ViewModels need to talk to each other. Mmm that is a bit of an issue since the views do not know about each other, or the ViewModel(s) about each other, so how can we perform messaging between ViewModels.
Luckily help is at hand, via the use of the Mediator pattern. The Mediator pattern can be thought of as an event aggregator that knows about messages and who the message results should be routed to. The basic idea is that for each message, those interested in getting a callback based on a particular messages data state change will register interest for particular messages. When a message data state changes those that registered interest for the changed message data, will be notified via some form of callback.
So how does all this translate to code. Well its is fairly simple, thanks again
to Action<T> delegates, we have a Mediator class that looks
like the following
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
namespace GeoPlaces
{
/// <summary>
/// Available cross ViewModel messages
/// </summary>
public enum ViewModelMessages { IsAuthenticatedUser = 1, NewPlaceAdded=2 };
/// <summary>
/// A message Mediator singleton to allow unconnected ViewModel to
/// send and receive messages
/// </summary>
public sealed class Mediator
{
#region Data
static readonly Mediator instance = new Mediator();
private volatile object locker = new object();
//specialized Dictionary (see the code for this)
MultiDictionary<ViewModelMessages, Action<Object>> internalList
= new MultiDictionary<ViewModelMessages, Action<Object>>();
#endregion
#region Ctor
static Mediator()
{
}
private Mediator()
{
}
#endregion
#region Public Properties
public static Mediator Instance
{
get
{
return instance;
}
}
#endregion
#region Public Methods
/// <summary>
/// Registers a Colleague to a specific message
/// </summary>
/// <param name="callback">The callback to use when the message it seen</param>
/// <param name="message">The message to register to</param>
public void Register(Action<Object> callback, ViewModelMessages message)
{
internalList.AddValue(message, callback);
}
/// <summary>
/// Notify all colleagues that are registed to the specific message
/// </summary>
/// <param name="message">The message for the notify by</param>
/// <param name="args">The arguments for the message</param>
public void NotifyColleagues(ViewModelMessages message, object args)
{
if (internalList.ContainsKey(message))
{
//forward the message to all listeners
foreach (Action<object> callback in internalList[message])
callback(args);
}
}
#endregion
}
}
It can be seen that the Mediator simply mantains a Dictionary of messages and
callbacks, and that the callback Action<Object> delegates
are invoked when a message data state changes, where the new state is passed
as an Object to the callback Action<Object> delegates. This
is done by the NotifyCollegues method of the Mediator, which looks through all
registered callback Action<Object> delegates, and calls them
passing the new state object. So all ViewModels that wish to communicate with
each other will use the Register() method of the Mediator and will
receive callback via the Action<Object> delegates that were
used when registering for the message data state changes.
Here is what the code for a ViewModel looks like when registering with the Mediator.
//register to the mediator for the IsAuthenticatedUser message
Mediator.Instance.Register(
(Object o) =>
{
isAuthenticatedUser = (Boolean)o;
if (isAuthenticatedUser)
GetAllPlaces();
}, ViewModelMessages.IsAuthenticatedUser);
And here is an example that updates a message data state, and notifies those interested that new data state is available.
Mediator.Instance.NotifyColleagues(
ViewModelMessages.IsAuthenticatedUser, true);
In this example the you can see that the callback delegate is actaully defined
as follows, and we must cast the Object parameter passed to the callback Action<Object>
delegates delegate to the expected type (Boolean in this case).
(Object o) =>
{
isAuthenticatedUser = (Boolean)o;
if (isAuthenticatedUser)
GetAllPlaces();
}
I think this method works well.
As mentioned in the Restful WCF Service section, RESTful WCF makes use of Http. Which is cool, but 9 times out of 10 you do not simply want to GET data, you will want to POST and DELETE data. In which case you are going to have to use some sort of API that allows you to work either with HTTP or you can use the WebChannelFactory class which allows you to use a RESTful service in much the same way that you would have used a SOAP based WCF service.
The WebChannelFactory
class is a special ChannelFactory that automatically adds the WebHttpBehavior
to the endpoint if it is not already present. Furthermore, it adds a default
WebHttpBinding to the endpoint if the binding is not explicitly
configured and the address is an HTTP or HTTPS address.
So you can see that the WebChannelFactory
class does most of the heavy lifting for our client code, all that we need to
do is really create a new WebChannelFactory
and that is pretty much job done. Of course we are good developers and we want
to create robust re-usable code that abstracts the notion of a Client ChannelFactory
into something more natural.
To this end I have developed the following RESTful WCF service proxy class which makes the whole process of using the WebChannelFactory, a lot easier and provides error handling.
using System;
using System.Configuration;
using System.Net;
using System.ServiceModel;
using System.ServiceModel.Web;
using GeoPlacesDataService;
using Microsoft.ServiceModel.Web;
namespace GeoPlaces
{
/// <summary>
/// The client proxy delegate, which is typically an anonomous delegate
/// in the actual client code
/// </summary>
public delegate void UseServiceDelegate(IGeoService proxy);
/// <summary>
/// This section of code was originally obtained from the following source
/// http://www.iserviceoriented.com/blog/post/Indisposable+-+WCF+Gotcha+1.aspx
///
/// It was subsequently changed in order to make it work with the web based
/// RestFul WCf service. This class should handle restarting the proxy in case
/// of a faulted channel
/// </summary>
public static class Service
{
#region Data
private static IClientChannel proxy = null;
public static ChannelFactory<IGeoService> _channelFactory = null;
#endregion
static Service()
{
try
{
Uri serviceUri = new Uri(
ConfigurationManager.AppSettings["GeoServiceEndpointAddress"]);
_channelFactory = new WebChannelFactory<IGeoService>(serviceUri);
_channelFactory.Endpoint.Behaviors.Add(
new ContentTypeBehaviour { ContentType = "text/xml" });
}
catch (Exception e)
{
ApplicationException ae = new ApplicationException(
"Error initiating WCF channel for The GeoService", e);
Console.WriteLine(String.Format("An exception occurred : {0}", ae));
throw ae;
}
}
#region Public Methods
public static void Use(UseServiceDelegate codeBlock)
{
bool success = false;
if (proxy != null && (proxy.State == CommunicationState.Opened ||
proxy.State == CommunicationState.Opening))
{
//do nothing, all ok
}
else
proxy = (IClientChannel)_channelFactory.CreateChannel();
//try to create the Proxy
try
{
codeBlock((IGeoService)proxy);
success = true;
}
//WebException is only avaiable WCF REST Starter Kit Preview 2
//http://aspnet.codeplex.com/Release/ProjectReleases.aspx?ReleaseId=24644
catch (WebProtocolException webExp)
{
Console.WriteLine(String.Format("An exception occurred : {0}",
webExp.Message));
throw new ApplicationException(
"A GeoService WebProtocolException occured", webExp);
}
catch (WebException ex)
{
using (System.IO.Stream respStream = ex.Response.GetResponseStream())
using(System.IO.StreamReader reader =
new System.IO.StreamReader(respStream))
Console.WriteLine(String.Format("An exception occurred : {0}",
reader.ReadToEnd()));
}
catch (FaultException fex)
{
Console.WriteLine(String.Format("An exception occurred : {0}",
fex.Message));
throw new ApplicationException(
"A GeoService FaultException occured", fex);
}
catch (Exception ex)
{
Console.WriteLine(String.Format("An exception occurred : {0}",
ex.Message));
throw new ApplicationException(
"A GeoService Exception occured", ex);
}
finally
{
if (!success && proxy != null)
proxy.Abort();
}
}
#endregion
}
}
One thing to note is the WebProtocolException which you will not
find as part of the standard .NET codebase, this is an extra class that is part
of the WCF
REST Starter Kit Preview 2. The starter kit contains a few good classes,
that you can use to get you started with RESTful WCF. This starter kit is not
a prerequisite as I have included the nessecary Dlls in this apps codebase.
You will see how this proxy helper class is used just below
As I just stated I have developed a nice easy to use RESTful WCF proxy class that greatly aids in the usage of a WebChannelFactory. So how do we go about using this helper class. Well it is actually quite simple lets see some POST/GET examples that are used against the demo codes RESTful WCF service shall we.
AuthenticateUser (POST)
Which is defined as follows within the RESTful WCF service
[OperationContract]
[WebInvoke(Method = "POST", UriTemplate = "User/Add/",
ResponseFormat = WebMessageFormat.Xml)]
Users AddUser(Users newUser);
And has an implementation that looks like the code shown below, where we persist the new user to the SQL server database using the ADO .NET entity framework. I am also using some defines to determine if we need to create HTTP response codes and create a new REST Uri for the new resource (it is polite, the user may actually be using a Browser so will appreciate the new Uri being created for the Users new resource they just added)
/// <summary>
/// Adds a user to the System
/// </summary>
public Users AddUser(Users newUser)
{
try
{
GeoPlacesEntities model = new GeoPlacesEntities();
model.AddToUsers(newUser);
model.SaveChanges();
Users userFromDb = model.Users.Where((u) =>
u.Name.ToLower().Equals(newUser.Name) &&
u.Password.ToLower().Equals(newUser.Password)
).First();
#if HTTP
OutgoingWebResponseContext ctx =
WebOperationContext.Current.OutgoingResponse;
ctx.SetStatusAsCreated(CreateNewUserUri(userFromDb));
#endif
return userFromDb;
}
catch(Exception ex)
{
//WebProtocolException is part of WCF REST Starter Kit Preview 2
throw new WebProtocolException(HttpStatusCode.BadRequest,
"Couldn't add new user", null);
}
}
So now let us look at the WPF Client code to call this using our proxy helper :
/// <summary>
/// Logs a user in, and returns the logged in user
///
/// Calls WCF Service method
/// [OperationContract]
/// [WebInvoke(Method = "POST", UriTemplate = "User/Add/",
/// ResponseFormat = WebMessageFormat.Xml)]
/// Users AddUser(Users newUser);
/// </summary>
public static Users RegisterUser(String username, String password)
{
Boolean isAuthenticatedUser = false;
Users currentUser = new Users
{
Name = username,
Password = password,
Places = null
};
Users dbReadUser = null;
try
{
//Use the GEOPlacesService Proxy
Service.Use((client) =>
{
//need OperationContextScope to use WebOperationContext(s)
//and HttpStatusCode(s)
using (new OperationContextScope((IContextChannel)client))
{
dbReadUser = client.AddUser(currentUser);
IncomingWebResponseContext rctx =
WebOperationContext.Current.IncomingResponse;
if (rctx.StatusCode == System.Net.HttpStatusCode.Created)
{
if (dbReadUser != null)
isAuthenticatedUser = dbReadUser.ID >= 0;
}
}
});
if (isAuthenticatedUser)
return dbReadUser;
else
{
Console.WriteLine("Error registering user");
return null;
}
}
catch (ApplicationException Ex)
{
return null;
}
}
the line that looks like the code just below, is the proxy helper actually
being used, where the client is the actual IClientChannel which
is created within the Service proxy helper class. Can you see it, again it uses
delegates, so the code to run is done against the IClientChannel
created within the Service proxy helper class. Also note that we MUST use the
OperationContextScope to allow us to examine the HTTP status codes
to work.
Service.User((client) => { }
For completeness let us also consider a GET
GetAllPlacesForUser (GET)
Which is defined as follows within the RESTful WCF service
[OperationContract]
[WebGet(UriTemplate = "/placesList/{userId}",
ResponseFormat = WebMessageFormat.Xml)]
List<Places> GetAllPlacesForUser(String userId);
And has an implementation that looks like this, where we obtains all the places for a user from the SQL server database using the ADO .NET entity framework.
/// <summary>
/// Gets all places for a particular user
/// </summary>
public List<Places> GetAllPlacesForUser(String userId)
{
Int32 id = -1;
if (Int32.TryParse(userId, out id))
{
try
{
GeoPlacesEntities model = new GeoPlacesEntities();
return model.Places.Where((pl) => pl.Users.ID == id).ToList();
}
catch (Exception ex)
{
//WebProtocolException is part of WCF REST Starter Kit Preview 2
throw new WebProtocolException(HttpStatusCode.BadRequest,
String.Format("Couldn't find places for user id {0}", userId), null);
}
}
else
return null;
}
Again we use the Service proxy helper class, which was the line, as before
we MUST use the OperationContextScope to allow us to examine the
HTTP status codes.
Service.User((client) => { }
/// <summary>
/// Logs a user in, and returns the logged in user
///
/// Calls WCF Service method
/// [OperationContract]
/// [WebGet(UriTemplate = "/placesList/{userId}",
/// ResponseFormat = WebMessageFormat.Xml)]
/// List<Places> GetAllPlacesForUser(String userId);
/// </summary>
public static ObservableCollection<Places> GetAllPlacesForUser(String userId)
{
Boolean completedOk = false;
ObservableCollection<Places> places = null;
List<Places> placesReturned = null;
try
{
//Use the GEOPlacesService Proxy
Service.Use((client) =>
{
//need OperationContextScope to use WebOperationContext(s)
//and HttpStatusCode(s)
using (new OperationContextScope((IContextChannel)client))
{
placesReturned = client.GetAllPlacesForUser(userId.ToString());
IncomingWebResponseContext rctx =
WebOperationContext.Current.IncomingResponse;
if (rctx.StatusCode == System.Net.HttpStatusCode.OK)
{
completedOk = placesReturned != null;
}
}
});
if (completedOk)
{
places = new ObservableCollection<Places>(placesReturned);
return places;
}
else
{
Console.WriteLine("Error getting all places for user");
return null;
}
}
catch (ApplicationException Ex)
{
return null;
}
}
All the ViewModels communicate with the RESTful WCF service via similiar methods to these 2 shown above, where all the actual WCF service calls are made via another helper class called "ServiceCalls", which does the RESTful WCF service calls shown above, and allows the ViewModel code to remain clean and free of any WCF service layer code/using statements.
As stated at various places within this article, I am using a hosted Microsoft Virtual Earth instance, which obviously means you have to have Microsoft Virtual Earth installed, but what about the control, which looks like this, how does that all work?

Well it is actually a bit of a cheat, it is a WPF control, I did not write this control, but I do know enough about it, to talk about it. It is by InfoStrat and is available at Virtual Earth 3D for WPF and Microsoft Surface (again my attached code includes all you need, you do not have to download it unless you want to)
The InfoStrat.VE control uses an internal D3DImage, which is a WPF control that can be used to host DirectX 3D content in a WPF application.
So what Infrostrat do is effectively use a IntPtr Handle to point
the DirectX rendering at a D3DImage
(I was using a very simliar idea when I started out using Google Earths COM
API). Here is the most important part of the InfoStrat.VE.Map code, where we
get the VE DirectX pointer to the 3D surface that will be used to host on the
WPF based D3DImage
/// <summary>
/// Gets pointer to the Virtual Earth D3D backbuffer
/// </summary>
/// <returns></returns>
private IntPtr GetSourceSurfacePtr()
{
GraphicsEngine3D graphicsEngine = GetGraphicsEngine();
if (graphicsEngine == null)
return IntPtr.Zero;
Microsoft.MapPoint.Graphics3D.Types.Surface surfSrc = null;
IntPtr ret = IntPtr.Zero;
try
{
surfSrc = graphicsEngine.Device.GetRenderTarget(0);
if (surfCpy == null)
{
CreateVESurface();
}
if (surfSrc != null && surfCpy != null)
{
graphicsEngine.Device.StretchRectangle(surfSrc,
new System.Drawing.Rectangle(0, 0, surfSrc.Description.Width,
surfSrc.Description.Height), surfCpy,
new System.Drawing.Rectangle(0, 0, surfSrc.Description.Width,
surfSrc.Description.Height));
Microsoft.MapPoint.GraphicsAPI.Graphics.Surface internalSurf = null;
internalSurf = ReadPrivateVariable<Microsoft.MapPoint.Graphics3D.Types.Surface,
Microsoft.MapPoint.GraphicsAPI.Graphics.Surface>(surfCpy,
"internalSurface");
if (internalSurf != null)
{
//NativePointer is hidden from intellisense
ret = internalSurf.NativePointer;
}
}
}
finally
{
if (surfSrc != null)
surfSrc.Dispose();
}
return ret;
}
Probably the best tutorial on using this D3DImage control is by Dr WPF (so you know it is 1,000,000% correct) and can be found at D3DImage.aspx
So by using the InfoStrat.VE.Map control, all that is left to do it create an instance of the control and have some buttons that can carry out various map functionality such as:
This is all done in code behind, I know I know that is not very MVVM now is it....Well in this case it just didn't seem to fit the MVVM pattern. Here is the code for all these map functions. I should point out that I had to resort to creating the actual InfoStrat.VE.Map in code behind as occassionally intellisense was lost even though the app compiled and ran correctly.
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Data;
using System.Windows.Documents;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Media.Imaging;
using System.Windows.Navigation;
using System.Windows.Shapes;
using InfoStrat.VE;
using System.Linq;
using GeoPlacesModel;
namespace GeoPlaces
{
/// <summary>
/// Virtual Earth control and a List of Places
/// </summary>
public partial class VEMapControl : UserControl, IView
{
#region Data
private PlacesViewModel placesViewModel = null;
private VEMap map = null;
private VEPushPin newPin = null;
#endregion
#region Ctor
public VEMapControl()
{
InitializeComponent();
SetUpMap();
this.Loaded += new RoutedEventHandler(VEMapControl_Loaded);
placesViewModel = new PlacesViewModel(this);
map.Show3DCursor = true;
//wire up PlaceClicked
this.AddHandler(PlaceControlDetailed.PlaceClickedEvent,
new PlaceClickedRoutedEventHandler(PlaceClicked));
}
#endregion
#region Private Methods
/// <summary>
/// Had to resort to setting up Map in code, as intellisense
/// was being lost (though all was ok at runtime) if map was
/// set up in XAML
/// </summary>
private void SetUpMap()
{
//Create the VE Map
map = new VEMap
{
Width = 590,
Height = 590,
Margin = new Thickness(5),
VerticalAlignment = VerticalAlignment.Top,
HorizontalAlignment = System.Windows.HorizontalAlignment.Center,
MapStyle = VEMapStyle.Hybrid,
LatLong = new Point(38.9444195081574, -77.0630161230201),
Clip = new EllipseGeometry
{
RadiusX = 230,
RadiusY = 230,
Center = new Point(295, 295)
}
};
//Ceeate a default pin location (my house)
newPin = new VEPushPin(new VELatLong(50.826958333333337,
-0.16388055555555556));
newPin.SetResourceReference(VEPushPin.StyleProperty, "PushPinStyle");
newPin.Content = new Label
{
Content = "Waiting",
HorizontalAlignment = HorizontalAlignment.Center,
FontSize = 20
};
newPin.Click += VEPushPin_Click;
map.Items.Add(newPin);
//I do not like doing this with indexes that may change, but
//I had no choice as I wanted map to be exactly 4th child, and
//when setting up map in XAML it would sometimes loose intellisense
mainGrid.Children.Insert(4,map);
}
private void VEMapControl_Loaded(object sender, RoutedEventArgs e)
{
this.DataContext = placesViewModel;
}
private void btnZoomIn_Click(object sender, RoutedEventArgs e)
{
map.DoMapZoom(1000, false);
}
private void btnZoomOut_Click(object sender, RoutedEventArgs e)
{
map.DoMapZoom(-1000, false);
}
private void BtnRoad_Click(object sender, RoutedEventArgs e)
{
map.MapStyle = InfoStrat.VE.VEMapStyle.Road;
}
private void BtnAerial_Click(object sender, RoutedEventArgs e)
{
map.MapStyle = InfoStrat.VE.VEMapStyle.Aerial;
}
private void BtnHybrid_Click(object sender, RoutedEventArgs e)
{
map.MapStyle = InfoStrat.VE.VEMapStyle.Hybrid;
}
private void VEPushPin_Click(object sender, VEPushPinClickedEventArgs e)
{
VEPushPin pin = sender as VEPushPin;
map.FlyTo(pin.LatLong, -90, 0, 300, null);
}
private void btnPanUp_Click(object sender, RoutedEventArgs e)
{
map.DoMapMove(0, 1000, false);
}
private void btnPanDown_Click(object sender, RoutedEventArgs e)
{
map.DoMapMove(0, -1000, false);
}
private void btnPanLeft_Click(object sender, RoutedEventArgs e)
{
map.DoMapMove(1000, 0, false);
}
private void btnPanRight_Click(object sender, RoutedEventArgs e)
{
map.DoMapMove(-1000, 0, false);
}
/// <summary>
/// Remove old pin and add new pin when user selects a place to view
/// </summary>
private void PlaceClicked(Object sender, PlaceClickedEventArgs args)
{
Places selectedPlace = args.PlaceSelected;
newPin.Latitude = selectedPlace.Latitude;
newPin.Longitude = selectedPlace.Longitude;
newPin.Content = new Label
{
Content = selectedPlace.Name,
HorizontalAlignment = HorizontalAlignment.Center,
FontSize = 20
};
map.FlyTo(new VELatLong(selectedPlace.Latitude,
selectedPlace.Longitude), -80, 0, 300, null);
}
#endregion
#region IView Members
public void ShowMessage(string message)
{
MessageBox.Show(message, "information",
MessageBoxButton.OK, MessageBoxImage.Information);
}
#endregion
}
}
The only really important part here is that there is an event handler set up to handle a previously SQL saved users Place being clicked. When a place is clicked from the list of places the map pin is moved to the new Latitude/Longitude positions, and the map is flown to the pin new position. It's cool I think.
Oh one thing worth a mention is that by default the InfoStrat.VE.Map
control is square, but by using a Ellipse for the Clip, we effectively
clip the control to be an Ellipse.
When I first started this article I was using Google Earths COM API and hosting that in a WinForms control, which was then hosted via Interop in a WPF app, which I did using the following code, if any one is interested. As soon as I saw the Infostrat.VE.Map stuff, Googles Earths COM API just seemed a bit naff, and hacky. I think Google Earths Web control is better though.
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Runtime.InteropServices;
namespace GoogleEarthControl
{
public class Win32
{
[DllImport("user32", CharSet = CharSet.Auto)]
public extern static IntPtr GetParent(IntPtr hWnd);
[DllImport("user32", CharSet = CharSet.Auto)]
public extern static bool MoveWindow(IntPtr hWnd,
int X, int Y, int nWidth, int nHeight, bool bRepaint);
[DllImport("user32", CharSet = CharSet.Auto)]
public extern static IntPtr SetParent(IntPtr hWndChild,
IntPtr hWndNewParent);
[DllImport("user32", CharSet = CharSet.Auto)]
public extern static IntPtr PostMessage(int hWnd,
int msg, int wParam, int IParam);
[DllImport("user32", CharSet = CharSet.Auto)]
public extern static bool SetWindowPos(int hWnd,
IntPtr hWndInsertAfter, int X, int Y, int cx, int cy, uint uFlags);
[DllImport("coredll.dll", CharSet = CharSet.Auto, SetLastError = false)]
public static extern IntPtr SendMessage(IntPtr hWnd, uint Msg, IntPtr wParam, IntPtr lParam);
public static readonly Int32 WM_QUIT = 0x0012;
public static readonly IntPtr HWND_TOP = new IntPtr(0);
public static readonly IntPtr HWND_BOTTOM = new IntPtr(1);
public static readonly UInt32 SWP_HIDEWINDOW = 128;
public static readonly UInt32 SWP_SHOWWINDOW = 64;
public static readonly uint WM_SYSCOMMAND = 0x0112;
public static readonly int SC_CLOSE = 0xF060;
public static IntPtr GEHrender = (IntPtr)5;
}
}
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Diagnostics;
using System.Drawing;
using System.Data;
using System.IO;
using System.Linq;
using System.Text;
using System.Windows.Forms;
using EARTHLib;
using System.Runtime.InteropServices;
namespace GoogleEarthControl
{
public partial class WinFormGEContainerControl : UserControl
{
private ApplicationGEClass googleEarth;
private IntPtr mainWindowPtr = (IntPtr)(-1);
public WinFormGEContainerControl()
{
InitializeComponent();
}
private void WinFormGEContainerControl_Load(object sender, EventArgs e)
{
mainWindowPtr = this.Handle;
googleEarth = new ApplicationGEClass();
Win32.GEHrender = (IntPtr)googleEarth.GetRenderHwnd();
Win32.MoveWindow(Win32.GEHrender, 0, 0, (int)this.Width,
(int)this.Height, true);
Win32.SetParent(Win32.GEHrender, mainWindowPtr);
Win32.SetWindowPos(googleEarth.GetMainHwnd(), Win32.HWND_BOTTOM,
10, 10, 10, 10, Win32.SWP_HIDEWINDOW);
}
public void StopGE()
{
try
{
Win32.SendMessage((IntPtr)googleEarth.GetMainHwnd(),
Win32.WM_SYSCOMMAND,
(IntPtr)Win32.SC_CLOSE, (IntPtr)0);
}
catch (Exception)
{
//Ok P/Invoke close didn't work, so have no choice but to kill process
Process[] p = Process.GetProcessesByName("googleearth");
if (p.Length > 0)
{
try
{
p[0].Kill();
}
catch (Exception)
{
Console.WriteLine("There was a problem shutting down googleearth");
}
}
}
finally
{
try
{
if (googleEarth != null)
Marshal.ReleaseComObject(googleEarth);
}
catch (ArgumentException argEx)
{
Console.WriteLine("There was a problem shutting down googleearth");
}
}
}
}
}
As I stated earlier the WPF Client makes use of a 3D flipping control that allows the user to have 2 interactive controls hosted on the front and back of a 3D mesh and allows the user to flip this in 3D space. I actually used this technique a while back in my MyFriends app it was cool but a little messy, and it required the user to know a bit about 3D in general.
Now luckily one of my WPF heros/mates and general WPF aficionado Mr Josh Smith, did a great job of abstracting this into a single easy to use 3D WPF control. Josh is calling his 3D library Thriple and you can read all about it over at the Thriple site.
Of course the WPF Client makes heavy usage of Styles/Templates, but that is all bulk standard WPF stuff, if you want to know about that just jump into the code.
There is a kinda nice pulsing Ring control, cunningly named "PulsingRingControl".
I would just like to mention that this article has taken me about 1 month to write in my spare time, so any votes/comments would be gratefully receieved. Anyway I hope you got something out of the article, and that it taught you a little something.
Like I say also stay tuned for more articles on WPF Onyx, it is a very promising framework, well done Bill.
General
News
Question
Answer
Joke
Rant
Admin
|
PermaLink |
Privacy |
Terms of Use
Last Updated: 8 Jun 2009 Editor: |
Copyright 2009 by Sacha Barber Everything else Copyright © CodeProject, 1999-2009 Web21 | Advertise on the Code Project |