DarksideCookie

Come to the dark side...we have cookies!

Silverlight and non-IIS hosted WCF-services

I’m currently working on a kiosk application for a company called TicketDirect. They sell tickets to different events all around New Zealand and have decided to create a kiosk based solution where customers can buy tickets. The application needs to be Silverlight based for a couple of reasons, and must run Silverlight 3 as 4 doesn’t have a go-live license yet. The problem with this is that the application needs to talk to a credit/debit card unit as well as a ticket printer. The problem as you probably realize quickly, is that these units are connected to the computer physically and therefore are not available to Silverlight. And yeah…as you know, printing support is “limited” in Silverlight, so printing custom ticket layouts to a special ticket printer will not work…

This is how I solved it. I created a couple of WCF services to run locally on the machine and then had Silverlight connect to those to access the local system. The problem was that I did not want to run IIS on the local machines. Why? Well, for different reasons…such as the need for simple XCopy deployment. So the services would be hosted by a Windows application instead.

The service I am going to be using needs to be hosted somewhere. In this case I want it simple, so I create a console application. When it project is created, it creates little more than a Program.cs file, which is nice and simple.

Let’s add a WCF service to the console application project. The service will be ridiculously simple and not access anything locally or so, but it will show the hosting part of it. When adding a WCF service to a console application, there is no nice little “Silverlight-enabled WCF Service”, which is good and bad… The bad part is that we need to create a regular WCF service and modify it a little. The good part is that we get a nicer implementation. Nicer implementation? Well, check this blog post for more information about it…

Anyhow…I add a WCF service called TimeService. That creates an interface, a class implementing the interface and a configuration file. Let’s change the interface to look like this

[ServiceContract]
public interface ITimeService
{
[OperationContract]
DateTime GetTime();
}

We then need to change the class that implements the interface in the same way and create a simple implementation

public class TimeService : ITimeService
{
public DateTime GetTime()
{
return DateTime.Now;
}
}

 

Unfortunately we need to make some other changes to the implementation as well to make it accessible from Silverlight. We first need to add an attribute called AspNetCompatibilityRequirements to the service code. This makes it “Silverlight-enabled”.  This is one part of what the “Silverlight-enabled WCF Service” template fixes for us.

[AspNetCompatibilityRequirements(RequirementsMode = AspNetCompatibilityRequirementsMode.Allowed)]
public class TimeService : ITimeService
{
public DateTime GetTime()
{
return DateTime.Now;
}
}

 

To get the AspNetCompatibilityRequirements to work, you need to add a using statement for System.ServiceModel.Activation. This can either be done manually or by writing the name of the attribute with the correct spelling and casing and then press Ctrl+. and then Enter…

So…that’s it for implementing the service. But before we look at the hosting side of it, I need to make some changes to the configuration. My configuration looks as below. All I have done is that I have changed the binding attribute of my endpoint to use basicHttpBinding instead of the standard wsHttpBinding. I also changed the baseAddress to go to a simpler address than the default one.

<?xml version="1.0" encoding="utf-8" ?>
<configuration>
<system.serviceModel>
<behaviors>
<serviceBehaviors>
<behavior name="DarksideCookie.WCFHosting.ServiceConsole.TimeServiceBehavior">
<serviceMetadata httpGetEnabled="true" />
<serviceDebug includeExceptionDetailInFaults="false" />
</behavior>
</serviceBehaviors>
</behaviors>
<services>
<service behaviorConfiguration="DarksideCookie.WCFHosting.ServiceConsole.TimeServiceBehavior"
name="DarksideCookie.WCFHosting.ServiceConsole.TimeService">
<endpoint address="" binding="basicHttpBinding" contract="DarksideCookie.WCFHosting.ServiceConsole.ITimeService">
<identity>
<dns value="localhost" />
</identity>
</endpoint>
<endpoint address="mex" binding="mexHttpBinding" contract="IMetadataExchange" />
<host>
<baseAddresses>
<add baseAddress="http://localhost:8731/TimeService/" />
</baseAddresses>
</host>
</service>
</services>
</system.serviceModel>
</configuration>

 

Now it is time to take a look at how you host a WCF service in a console application. The secret is a simple class called ServiceHost. It might not be that simple since it handles threading and things like that, but it is simple to work with. It is in the System.ServiceModel namespace. Just create a new instance, passing in either a Type or an instance of the service it should host. Then call Open() to start the service and Close() to stop it. And as we are in a console application, we need to keep the application running by using a Console.ReadKey() call…

class Program
{
static void Main(string[] args)
{
ServiceHost host = new ServiceHost(typeof(TimeService));
host.Open();
Console.WriteLine("Listening...press any key to close");
Console.ReadKey();
host.Close();
}
}

 

If you try running this, you might get it up and running and everything is fine. You might also get an exception saying that the application is not allowed to open a port. If you get an exception, you need to close down your VS and start it up running as an administrator. Annoying, but safe… :)

Next up, we need a Silverlight application to test it. So I add a new Silverlight application, but skip the accompanying test web site and just leave it up to VS to create a test page for me…

Then I press Ctrl+F5 to start the console application without debugging so that I can add my service reference to my Silverlight application. I then right click the “References” folder in the Silverlight project and choose “Add Service Reference…”. In the “Add Service Reference” window I write the address to my service, in this case http://localhost:8731/TimeService/, and press “Go”. Next I select the TimeService and press “OK”. This will create a nice service client for me that I can use. If you need to modify what it creates, this can be changed by clicking the “Advanced…” button. In there you can define some settings that change what code is auto generated. But for now, it will just be default settings and a name of ServiceReference1.

Next I focus on my Xaml. I create a simple Button, a TextBlock and an event handler for the Button’s Click event.

<UserControl x:Class="DarksideCookie.WCFHosting.SilverlightClient.MainPage"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
mc:Ignorable="d" d:DesignWidth="640" d:DesignHeight="480">
<Grid x:Name="LayoutRoot">
<StackPanel HorizontalAlignment="Center" VerticalAlignment="Center">
<TextBlock x:Name="Time" />
<Button Content="Click Me" Click="Button_Click" />
</StackPanel>
</Grid>
</UserControl>

Next I implement my event handler. In it, I create a new instance of the ServiceReference1.TimeServiceClient class, attach a handler for the GetTimeCompleted event and then call the GetTimeAsync method.

In the GetTimeCompleted  event handler, I set the text of the TextBlock to the returned time.

public partial class MainPage : UserControl
{
public MainPage()
{
InitializeComponent();
}

private void Button_Click(object sender, RoutedEventArgs e)
{
ServiceReference1.TimeServiceClient svc = new ServiceReference1.TimeServiceClient();
svc.GetTimeCompleted += svc_GetTimeCompleted;
svc.GetTimeAsync();
}

void svc_GetTimeCompleted(object sender, ServiceReference1.GetTimeCompletedEventArgs e)
{
Time.Text = e.Result.ToString();
}
}

And to get both of the applications running when I press F5, I open the properties for the solution, check the “Multiple startup projects” and define that both the projects should start.

Then I press F5 to start it all and see if it works… Oops…remember to close down the Console that was opened before… F5 again… And you get a warning from Visual Studio saying that webservice calls will fail unless hosted in the same application as the Silverlight application… Just press “Yes”.

Pressing the button should now give us the time, but instead it will result in an error. Saying something like

An error occurred while trying to make a request to URI 'http://localhost:8731/TimeService/'. This could be due to attempting to access a service in a cross-domain way without a proper cross-domain policy in place, or a policy that is unsuitable for SOAP services…”

And that is so true. If you were to run Fiddler or something similar, you would see that Silverlight is trying to access a clientaccesspolicy.xml located at http://localhost:8731. And it will of course not find one, because there isn’t one, so the call fails. This needs to be handled ASAP to get everything working…

So let’s start by adding that pesky clientaccespolicy.xml file to the project. I just add a regular XML file, availably under the Data node in the Add New Item dialog. I then add the following XML to it

<?xml version="1.0" encoding="utf-8"?>
<access-policy>
<cross-domain-access>
<policy>
<allow-from http-request-headers="*">
<domain uri="*"/>
</allow-from>
<grant-to>
<resource path="/" include-subpaths="true"/>
</grant-to>
</policy>
</cross-domain-access>
</access-policy>

Finally I change the Build Action to embedded resource.

Next I add a new WCF Service to the project. The interface is simple. It has a single method that I call GetClientAccessPolicy() that returns a Stream. I also need to add an extra attribute, called WebGet, to it. The WebGet attribute is available in the System.ServiceModel.Web assembly, so I ad a reference to it before adding a using statement and finally adding the attribute to the method.

[ServiceContract]
public interface ISilverlightService
{
[OperationContract, WebGet(UriTemplate = "/clientaccesspolicy.xml")]
Stream GetClientAccessPolicy();
}

 

The WebGet attribute makes it possible to call a WCF service with a normal HTTP GET call. At least after some extra configuration… :)

The implementation of the GetClientAccessPolicy() method is simple. Just pull out the resource stream from the assembly and return it…

public class SilverlightService : ISilverlightService
{
public System.IO.Stream GetClientAccessPolicy()
{
return Assembly.GetExecutingAssembly()
.GetManifestResourceStream("DarksideCookie.WCFHosting.ServiceConsole.ClientAccessPolicy.xml");
}
}

 

Next, I modify and clean up the App.config file. First of all, they both share the same service behavior, so I remove one of the available ones and rename the other to DefaultServiceBehavior. I the update service entries with the changed behavior. Next I need to modify the service entry for the Silverlight service. It needs to answer at the root of the host, so I change the base address to http://localhost:8731/. It also needs a different endpoint behavior and binding. SO I add a new endpoint behavior, called webHttpBehavior, to the file. Next I change the endpoint entry so that the behavior is set to webHttpBehavior and the binding is set to webHttpBinding. This configuration makes it possible to call the service using a HTTP GET call.

The completed config file looks like this

<?xml version="1.0" encoding="utf-8" ?>
<configuration>
<system.serviceModel>
<behaviors>
<endpointBehaviors>
<behavior name="webHttpBehavior">
<webHttp />
</behavior>
</endpointBehaviors>
<serviceBehaviors>
<behavior name="DefaultServiceBehavior">
<serviceMetadata httpGetEnabled="true" />
<serviceDebug includeExceptionDetailInFaults="false" />
</behavior>
</serviceBehaviors>
</behaviors>
<services>
<service behaviorConfiguration="DefaultServiceBehavior" name="DarksideCookie.WCFHosting.ServiceConsole.SilverlightService">
<endpoint address="" behaviorConfiguration="webHttpBehavior"
binding="webHttpBinding" contract="DarksideCookie.WCFHosting.ServiceConsole.ISilverlightService">
<identity>
<dns value="localhost" />
</identity>
</endpoint>
<host>
<baseAddresses>
<add baseAddress="http://localhost:8731/" />
</baseAddresses>
</host>
</service>
<service behaviorConfiguration="DefaultServiceBehavior"
name="DarksideCookie.WCFHosting.ServiceConsole.TimeService">
<endpoint address="" binding="basicHttpBinding"
contract="DarksideCookie.WCFHosting.ServiceConsole.ITimeService">
<identity>
<dns value="localhost" />
</identity>
</endpoint>
<endpoint address="mex" binding="mexHttpBinding" contract="IMetadataExchange" />
<host>
<baseAddresses>
<add baseAddress="http://localhost:8731/TimeService/" />
</baseAddresses>
</host>
</service>
</services>
</system.serviceModel>
</configuration>

Next, I press F5 to start the application again. But this time, pressing the button should cause the current date and time to be displayed in the window above the button…

Mission accomplished! Hosting WCF services outside of IIS. As long as we get the clientaccesspolicy.xml file hosted like this, we can host as many services we want at that host. We can also take the WebGet attribute a step longer and create a really funky “windows based” Silverlight application…sort of… But more about that in the next post…

Source code available here: WCF Service Console.zip (91.63 kb)

Cheers!

Posted: Jan 24 2010, 22:12 by ZeroKoll | Comments (17) |
  • Currently 0/5 Stars.
  • 1
  • 2
  • 3
  • 4
  • 5
Manage post: :)

Comments (17) -

James D United States said:

This looks great but with the download I'm still getting the cross domain error - any hints as to where to look or how to fix that?
thanks

# April 29 2010, 15:33

James D United States said:

Just as a follow on to this - I've just discovered that it is working fine in Internet Explorer but not in Firefox - any thoughts?

# April 29 2010, 15:51

ZeroKoll New Zealand said:

That is really weird. THey should behave the same way. What happens if you run Fiddler and check to see what calls Firefox makes? Silverlight's networking goes through the browser and I don't know if Firefox has issues with switching between domains that are in different security area (internet vs local). Interesting thing though...I will try to find time to check it out...

# April 30 2010, 13:54

Job Netherlands said:

I can confirm aswell that Firefox still gives the Cross-Domain and IE doesn't.

Is there a solution already?

# May 10 2010, 12:03

ZeroKoll New Zealand said:

Hi Job!
Unfortunately I don't know of a solution. It is not really Silverlight fault, but rather FireFox. Silverlight piggybacks on the browsers network support, so if there is a problem with it, it is most likely the browsers implementation and not Silverlight. But that doesn't really help, does it...
Have you checked fiddler?
On the other hand, this should only be used in pretty extreme cases, where you have control over the browser choice. Just use IE...

# May 11 2010, 07:57

Jason Allred United States said:

I've integrated this into both a console application and a windows service.  Oddly the console app will run great, but the service will give "[Fiddler] Connection to dispatcher failed.<BR>Exception Text: No connection could be made because the target machine actively refused it"

I've gone over the code and config files.  The config files are exactly the same, as is the code (with the exception of how they start).  The only difference I've come up with is that the Console app runs under my login, and the other one runs under a login created just for it.

My question is, am I on the right track, or grasping at straws?

# August 02 2010, 20:30

Job Netherlands said:

You have to use an account with admin permissions.
I'm using the local system account.

# August 03 2010, 08:22

ZeroKoll New Zealand said:

It's just as Job is saying. You have to run under an account that has administrative privileges. Otherwise, it won't be allowed to open ports for the service...
Cheers,
Chris

# August 03 2010, 10:34

Sundar India said:

Thanks for the post. I tried a sample with this post as base. When I try to create the WCF REST service (Silverlight service), I am not able to define the 'WebGet' attribute. It throws 'IOperationBehavior' error. I understand that WebGetAttribute is defined in 'System.ServiceModel.Web.Extensions.dll' (Silverlight Client Profile) or 'System.Web.Dll'. I tried both to no use. Can someone help me out here? How were you able to define a RESTful web service in Console application? Please let me know.

# August 05 2010, 03:05

ZeroKoll New Zealand said:

Hi Sundar!
Have you tried downloading my sample source and check what I have done?
// Chris

# August 08 2010, 02:11

Sundar India said:

Hi Chris,

VS 2010 has a .Net Framework client profile and .Net Framework profile. The Webget is not working in the client profile. And the 'System.ServiceModel.Web' was available on the .Net Framework profile. Thanks Chris. It worked.

Thanks
Sundara Kumar Padmanabhan

# August 08 2010, 04:11

ZeroKoll New Zealand said:

Yeah...sorry I didn't think of that. It isn't the first time the client profile has stuffed up my code... Good that you found it!

# August 08 2010, 04:42

Jason Allred United States said:

Hi,

I get a new error, and can't find much on how to solve it.  Any Suggestions?

<E2ETraceEvent xmlns="http://schemas.microsoft.com/2004/06/E2ETraceEvent">
<System xmlns="http://schemas.microsoft.com/2004/06/windows/eventlog/system">
<EventID>131075</EventID>
<Type>3</Type>
<SubType Name="Error">0</SubType>
<Level>2</Level>
<TimeCreated SystemTime="2010-08-12T21:34:07.2333820Z" />
<Source Name="System.ServiceModel" />
<Correlation ActivityID="{00000000-0000-0000-0000-000000000000}" />
<Execution ProcessName="CadServerConsole" ProcessID="11288" ThreadID="265" />
<Channel />
<Computer>L23204</Computer>
</System>
<ApplicationData>
<TraceData>
<DataItem>
<TraceRecord xmlns="http://schemas.microsoft.com/2004/10/E2ETraceEvent/TraceRecord" Severity="Error">
<TraceIdentifier>http://msdn.microsoft.com/en-US/library/System.ServiceModel.Diagnostics.ThrowingException.aspx</TraceIdentifier>
<Description>Throwing an exception.</Description>
<AppDomain>CadServerConsole.exe</AppDomain>
<Exception>
<ExceptionType>System.ServiceModel.ProtocolException, System.ServiceModel, Version=3.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</ExceptionType>
<Message>There is a problem with the XML that was received from the network. See inner exception for more details.</Message>
<StackTrace>
at System.ServiceModel.Channels.HttpRequestContext.CreateMessage()
at System.ServiceModel.Channels.HttpChannelListener.HttpContextReceived(HttpRequestContext context, ItemDequeuedCallback callback)
at System.ServiceModel.Channels.SharedHttpTransportManager.OnGetContextCore(IAsyncResult result)
at System.ServiceModel.Channels.SharedHttpTransportManager.OnGetContext(IAsyncResult result)
at System.ServiceModel.Diagnostics.Utility.AsyncThunk.UnhandledExceptionFrame(IAsyncResult result)
at System.Net.LazyAsyncResult.Complete(IntPtr userToken)
at System.Net.LazyAsyncResult.ProtectedInvokeCallback(Object result, IntPtr userToken)
at System.Net.ListenerAsyncResult.WaitCallback(UInt32 errorCode, UInt32 numBytes, NativeOverlapped* nativeOverlapped)
at System.Threading._IOCompletionCallback.PerformIOCompletionCallback(UInt32 errorCode, UInt32 numBytes, NativeOverlapped* pOVERLAP)
</StackTrace>
<ExceptionString>System.ServiceModel.ProtocolException: There is a problem with the XML that was received from the network. See inner exception for more details. ---&gt; System.Xml.XmlException: The body of the message cannot be read because it is empty.
   --- End of inner exception stack trace ---</ExceptionString>
<InnerException>
<ExceptionType>System.Xml.XmlException, System.Xml, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</ExceptionType>
<Message>The body of the message cannot be read because it is empty.</Message>
<StackTrace>
at System.ServiceModel.Channels.HttpRequestContext.CreateMessage()
at System.ServiceModel.Channels.HttpChannelListener.HttpContextReceived(HttpRequestContext context, ItemDequeuedCallback callback)
at System.ServiceModel.Channels.SharedHttpTransportManager.OnGetContextCore(IAsyncResult result)
at System.ServiceModel.Channels.SharedHttpTransportManager.OnGetContext(IAsyncResult result)
at System.ServiceModel.Diagnostics.Utility.AsyncThunk.UnhandledExceptionFrame(IAsyncResult result)
at System.Net.LazyAsyncResult.Complete(IntPtr userToken)
at System.Net.LazyAsyncResult.ProtectedInvokeCallback(Object result, IntPtr userToken)
at System.Net.ListenerAsyncResult.WaitCallback(UInt32 errorCode, UInt32 numBytes, NativeOverlapped* nativeOverlapped)
at System.Threading._IOCompletionCallback.PerformIOCompletionCallback(UInt32 errorCode, UInt32 numBytes, NativeOverlapped* pOVERLAP)
</StackTrace>
<ExceptionString>System.Xml.XmlException: The body of the message cannot be read because it is empty.</ExceptionString>
</InnerException>
</Exception>
</TraceRecord>
</DataItem>
</TraceData>
</ApplicationData>
</E2ETraceEvent>

# August 12 2010, 23:56

Vipul Patel India said:


Wonderful..............

this way i can practice on my windows 7 home edition without IIS

thanks a lot...

# August 13 2010, 20:58

Raj United States said:

I have same requirement as yours. I am getting Security Error with cross domain Exception(general). I used Fiddler to track and getting http response 200 (i.e good) at clientaccesspolicy. Any Idea?

Thanks
Raj

# November 12 2010, 00:42

James United Kingdom said:

If you are having problems with browsers other than IE accepting the clientaccesspolicy xml file (problems identified with Firefox in earlier comments), then simply add the following line prior to returning the policy file in SilverlightService.GetClientAccessPolicy()

WebOperationContext.Current.OutgoingResponse.ContentType = "application/xml";

# September 21 2011, 13:17

Sergey Russia said:

Chris, thanks you very much! I spent many hours trying to solve
this problem. You example is very clear. All goods for you (sorry about my english)

# January 19 2012, 04:43

Pingbacks and trackbacks (1)+

Comments are closed