2012-04-30

DPWS Client through WCF

In the last post I discussed creation of the host application on the device. In this post I'd like to finish off the example by delving into the client side, on the PC, using WCF. We'll start by creating a new Windows Forms Application. Really, any normal Windows application that supports full .NET Framework would do. But I like the idea of making a GUI based application that can be extended in the future to show the status of, and interact with, our Gadgeteer device.

In the new form, I make two buttons and two labels. The first button will be to manually initiate connection with the device, and the second will be to invoke our example method: getButtonValue. The labels with show the statuses of the two actions.

Next we need to auto-generate code for the client side, in a manner similar to that which we used for the device host. But, this time we need to use a different tool: SvcUtil.exe. Just like the host example, run the utility on the WSDL file. Unlike the MFSvcUtil, this one has a few extra parameters. Ignore most of them. The command to generate the code is:
D:\[...]\WcfDpwsClientExample>svcutil ..\GadgeteerDpwsHostExample\GadgeteerReportingService.wsdl /language:c#
If you followed along with my example code, the utility will generate a warning. It complains that my namespaces don't have a fully formed uri (i.e. http:// prefix). Whatever... We'll make the whole thing work just fine without uri namespaces.

Next we need to create a proper config file to enable the application to connect to the device host. We could do this manually, but there is a wonderful utility to do it for us automatically. I mentioned it in the former blog-post: SvcConfigEditor.exe. Open the auto generated output.config file in this utility. Remember, use the utility from the NETFX 4.0 Tools folder. The file we opened will just serve as a loose guideline. We will pretty much recreate a new file.

First, open the endpoint information in: Client/Endpoints. There should be a single auto-generated port in there. Most likely with a name like: gadgeteerStatusesBinding_GadgeteerStatuses. This is fine, but I will rename mine to gadgeteerStatusesPort. This is the name you give your Client object when initializing it, so it knows which section of the config file to read.

Secondly, go into Bindings section. There should be a single binding in there, with two extensions under it. We need to recreate these two extensions. So go ahead and delete them and then re-add them. The first was textMessageEncoding. Under its properties, ensure that MessageVersion is set to Soap12WSAddressing10. The second extension was httpTransport. Just add it and leave it at default settings.

Now save the file as app.config and add it to your project. The wonderful thing about the utility you just used is the ability to enable logging under Diagnostics section. This will allow you to save logs of all your Soap transactions and then view these logs visually with yet another tool--the name of which currently escapes me, but if you look into MSDN documentation for transaction logging, you will quickly find the name.

Next comes a tricky part. Add the auto-generated code file to your project. In order to stop VS from complaining about types, we'll need to add a reference to System.ServiceModel. There are several changes we need to do in order to communicate successfully with the device host. First and foremost, we need to end up with DataContractSerialization. The way that MFSvcUtil auto-generated its code. But if we tell SvcUtil to give us DataContractSerialization, it will have a mental meltdown and give us useless code. So, let it generate XmlSerialization and we'll have to fix it manually.

Find GadgeteerStatuses interface and replace:
[System.ServiceModel.XmlSerializerFormatAttribute()]
With:
[System.ServiceModel.DataContractFormatAttribute(Style=System.ServiceModel.OperationFormatStyle.Document)]
Then  find ButtonValueType class and add an attribute to it:
[System.Runtime.Serialization.DataContractAttribute( Namespace = "GadgeteerReportingService/gadgeteerStatuses" )]
Inside of the class, locate the Value property and add an attribute to it:
[System.Runtime.Serialization.DataMemberAttribute( Order = 0 )]
For the last two, you'll need to add a reference to System.Runtime.Serialization assembly. While you're at it, add one to System.ServiceModel.Discovery, as you'll need it in the next step.

We are ready to write the actual work code in our form now. I added two members:
Uri serviceAddress;
GadgeteerStatusesClient client;
Then, added a new method (blatantly copied from the .NETMF examples mentioned in my previous post):
bool FindService()
{
    try
    {
        DiscoveryClient discoveryClient =
            new DiscoveryClient( new UdpDiscoveryEndpoint( DiscoveryVersion.WSDiscoveryApril2005 ) );

        Collection services = discoveryClient.Find( new FindCriteria( typeof( GadgeteerStatuses ) ) ).Endpoints;

        discoveryClient.Close();

        if ( services.Count == 0 )
        {
            return false;
        }
        else
        {
            serviceAddress = services[ 0 ].ListenUris[ 0 ];
        }
    }
    catch
    {
        return false;
    }

    return true;
}
Then after adding a handler for the connection button, we give it a body:
private void buttonConnect_Click( object sender, EventArgs e )
{
    labelConnect.Text = "Searching...";

    while ( !FindService() )
    {
        Console.WriteLine( "GadgeteerStatuses service not found.  Trying again..." );
        System.Threading.Thread.Sleep( 1000 );
    }

    // perform WebServices discovery
    client = new GadgeteerStatusesClient( "gadgeteerStatusesPort", serviceAddress.AbsoluteUri );
    labelConnect.Text = "Connected";
}
And finally, after adding a handler for the value fetching button, we fill its body:
private void buttonValue_Click( object sender, EventArgs e )
{
    if ( client == null )
        return;

    try
    {
        ButtonValueType bv = client.getButtonValue( new EmptyValueType() );
        labelValue.Text = bv.Value.ToString();
    }
    catch ( Exception ex )
    {
        System.Diagnostics.Debug.Print( "Web Service call returned exception with message: " + ex.Message );
    }
}

From what I understand, the discovery will search active web services available on the network, for all that implement GadgeteerStatuses interface. Once it finds some, it will assign the first one to our form variable. We will then use the configuration file settings to connect to it and exchange data. If you followed the steps in these two posts, you should be able to connect, and then query the device for the status of the button. If the button is currently not depressed, it should return 0, and if it's depressed, it should return 1.

Have fun with it.

No comments:

Post a Comment