ON TARGET

s o f t w a r e

www.targetsoft.com

        C# 2008 / ASP.NET 3.5

         Data Service to Silverlight

         Revision 1.0

 

Copyright 2008    On Target Software   All Rights Reserved


Table of Contents

Overview

Create the Data Service

Naming conventions

The database

Create ASP.NET application

Create Database Remotely

Test Database Access

What if it doesn't work?

Create the Data Model

Create the Data Service

As a convenience, create a web.changes.config file

Create the Silverlight Client

Add a new project

Add a ListBox and a Button to supply dummy data

Add the Service Reference

Implement the data client

 


Overview

For this cookbook, I'll build a simple website implementing a data service and a Silverlight client, using a simple database.

The instructions are straightforward and there seems to be no reason to be concerned, much like driving a simple mountain road on a summer's day.  A few turns here and there, no big deal.  You hardly notice the lack of guardrails.

Now turn off your lights and drive the same road on a moonless night and it's a whole different story.  I can't add any guardrails, but I hope to shed some light on the road ahead.

To that end, I've gone out of my way to cause problems, see what error messages were triggered, and seek out the solutions on the web. 

Create the Data Service

I'm generally following the instructions given here and here with annotations that cover my situation and that resolve problems I've encountered.

Naming conventions

Before starting, it helps to know what you'll call the different projects, namespaces and directories, so the names make sense for their purpose and don't overlap where they would conflict.  Because my domain name is targetsoft and I want to write a Silverlight application named MetroSilver, these are my choices:

Solution                       Targetsoft

Web application          Targetsoft

Virtual directory         Targetsoft

Namespace                  Targetsoft

Data service                 MetroData

Data service model     MetroModel  (Metro is short for metronome, for my timekeeper application)

Data service entities   MetroEntities  (this is the type in the Data Service)

Silverlight app             MetroSilver

The database

Before starting, I defined two of the tables that I'll have in my SQL Server 2008 Express database.  These are MUser and MEvent.  The application will be a simple time-tracking utility where users sign on to indicate the hours they’ve spent on different projects.  As a test, I'll be displaying the list of users, so I've entered four users in the database.

Create ASP.NET application

Create a new ASP.NET web application, with a new solution in a new directory.  I'm adding this to SourceSafe as I'm a strong believer in using source control for any project with any longevity.

Since I'll be debugging with the live website on my real domain, I made a note in the default.aspx page that the website will be unavailable during experimentation.  I also added four buttons which I'll use to test the service, the client, and local and remote access to the database.  The buttons, for now, have no associated actions.

·     Test under debugger – succeeds.

Publish to precompiled directory.

·     Create a new folder called Precompiled that is a sister to the Targetsoft solution folder.

·     Publish to that folder with these options:

·     delete existing files prior to publish

·     copy only files needed to run

·     include files from App_Data (currently, there are none)

·     Create a new virtual directory on the development machine, named Targetsoft.  Did not change any of the default configurations. 

·     Test precompiled application – succeeds.

Publish to Infosaic shared host.

·     Delete existing data related to existing Targetsoft website.

·     Copy precompiled files to infosaic and test – succeeds.

To upload the data, I used the free copy of Core FTP.

Create Database Remotely

Find the Database Publishing Wizard

You'll need a copy of the Database Publishing Wizard to produce a script capable of uploading the database to Infosaic.  Version 1.1 is fine for the older versions of SQL Server and it's easy to find and download on the web.  If you have SQL Server 2008, you'll need version 1.2, which is nowhere to be found on the web.  It is, however, installed with Visual Studio 2008, but it's hidden.  Sometimes it's available as right-click after you set up an ERD, but that didn't work for me.

Look in the Visual Studio install directory, under …\90\Tools\Publishing, for the file " SqlPubWiz.exe".

Use the wizard to create a script

You'll need to create and then edit the script.  The tool creates a unicode file.  For my host (www.infosaic.com) I needed to save it as an ANSI file, which meant opening it in Notepad and using the save as… option.  Then I could import it to the database, creating the schema and populating it with data.  This could get tedious with large databases but it has the advantage of being compatible across versions.

Test Database Access

There's no point creating a data service if you don't have simple access to the database itself.  So I'll begin by testing that with an ADO.NET SQLConnection. 

Connection strings

This test primarily verifies the connection strings.  This website goes into all sorts of useful detail on the topic; well worth reading.  When you believe they're correct, put the connection strings in web.config like this:

  <connectionStrings>

    <add name="TargetsoftLocal"

     connectionString="Data Source=yourMachineName\SQLEXPRESS;Initial Catalog=yourDatabase;

      User ID=yourUserID;Password=yourPassword;MultipleActiveResultSets=True"/>

    <add name="TargetsoftRemote"

      connectionString="Data Source=hostIPAddress,hostPortNumber;

      Network Library=DBMSSOCN;Database= yourDatabase;

      User ID= yourUserID;Password= yourPassword;MultipleActiveResultSets=True "/>

  </connectionStrings>

The entry "Network Library=DBMSSOCN" tells SQL Server to use TCP/IP for the connection. 


Write the test functions and run the test

The following functions read a database and echo the contents to a text label on the web page.  It's obviously specific to the targetsoft schema, you can modify it as you need.

If an exception occurs, the BuildExceptionMessage will dive through as many as ten nested exceptions, building a message with all of them included.  This is helpful when you get an exception but the third inner exception records the actual problem.

Call ReadDB with the name of the connection string (in my case, TargetsoftLocal or TargetsoftRemote) from inside the click handlers for the buttons on the web page.

        void ReadDB(string cnName)

        {

           try

           {

               SqlConnection cn = new SqlConnection();

               string cnStr = ConfigurationManager.ConnectionStrings[cnName].ConnectionString;

               Display("Connection string = \"" + cnStr + "\"");

               cn.ConnectionString = cnStr;

               cn.Open();

 

               string query = "Select * From MUser";

               SqlCommand sqlCommand = new SqlCommand(query, cn);

               SqlDataReader reader;

               reader = sqlCommand.ExecuteReader(CommandBehavior.CloseConnection);

 

               while (reader.Read())

               {

                   string message = reader["UserName"].ToString() + " ";

                   message += reader["FirstName"].ToString() + " ";

                   message += reader["LastName"].ToString();

                   Display(message);

               }

               reader.Close();

               Display("End of Test");

           }

           catch (Exception ex)

           {

               Display(BuildExceptionMessage("Exception in ReadDB.", ex));

               throw ex;

           }

        }

        void Display(string message)

        {

           MessageLabel.Text += message + "     ";

        }

        string BuildExceptionMessage(string message, Exception ex)

        {

           System.Text.StringBuilder sb = new System.Text.StringBuilder(message, 4096);

           sb.Append("\n");

 

           int i = 1;

           sb.AppendFormat("Exception level {0}: {1}", i++, ex.Message);

           for (; i < 11 && ex.InnerException != null; ++i)

           {

               ex = ex.InnerException;

               sb.Append("-------------------\n");

               sb.AppendFormat("Exception level {0}: {1}", i, ex.Message);

           }

           return sb.ToString();

        }

·     Test local database under debugger  – succeeds

·     Publish and test local database – succeeds

You can test remotely from your own machine, indeed, you can test access to the Infosaic-hosted database from inside the debugger since the hosted database is available from an IP address.  So I tested from the locally published website and that worked.  You'll find the data service is not nearly this transparent.

Now we need to run the acid test.  Upload the web app to the infosaic host and test the hosted database.  This is how the data service will access the database.

What if it doesn't work?

For basic web site creation and testing, I haven't given any description of what can go wrong, since this tends to be well-covered elsewhere.  From this point forward, I'll be covering the kinds of errors that can occur and what to do about them.

The customErrors tag

The first error you see might look like this:

Server Error in '/' Application.

Description: An application error occurred on the server. The current custom error settings for this application prevent the details of the application error from being viewed remotely (for security reasons). It could, however, be viewed by browsers running on the local server machine.

Details: To enable the details of this specific error message to be viewable on remote machines, please create a <customErrors> tag within a "web.config" configuration file located in the root directory of the current web application. This <customErrors> tag should then have its "mode" attribute set to "Off".

As the error suggests in further detail (which you'll see if you get the error), you need to insert this line in your web.config file:

<customErrors mode="Off"/>

'Driver' is not a valid keyword

This happens if you're trying to get a SQL Server 2008 database working with an ODBC connection string.  See here for detailed instructions and look at the sample connection strings I listed above.

A network-related or instance-specific error

You might get this message on your web page:

A network-related or instance-specific error occurred while establishing a connection to SQL Server. The server was not found or was not accessible. Verify that the instance name is correct and that SQL Server is configured to allow remote connections. (provider: Named Pipes Provider, error: 40 - Could not open a connection to SQL Server)

In this case, your connection string is syntactically correct, but the software could not connect to SQL Server.  It helps to remember that SQL Server needs to establish some kind if interprocess communication between your code and its driver.  It typically does this through Named Pipes but can also use other methods, like TCP/IP.  The Data Source in the connection string tells how to find that driver.

If it can't find the driver, it gives this error, which generally means the Data Source is wrong.  If you gave an IP address, you may need a port number.  


Create the Data Model

Please refer to these MSDN notes.  We are just now starting on step 2.

·     Follow step 2 in the MSDN notes to add a new ADO.NET Entity Data Model.  I called my data model MetroModel.

·     When asked, choose Generate from database.  I chose my Targetsoft database using Windows authentication.  I asked for the connection settings to be saved as "MetroEntities."  Choose Tables but not Views or Stored Procedures.  I gave the model namespace as Targetsoft, for my own convenience so I don't have extra namespaces floating around unnecessarily.

·     After Entity Designer opens, get the properties for the entities and fiddle with the names until you have, e.g. MUserSet and MUser; MEventSet and MEvent.  The MSDN notes describe this in detail.

·     If you now look at Web.config, you'll find a connection string with parameters that will work only for your local database.  You'll need to modify this string every time you upload to your shared host.  For now, leave it alone, we'll deal with it later.

Create the Data Service

·     Once again, follow the MSDN notes and add a new Data Service, which I called MetroData.

·     Set the type of the DataService in the TestService class declaration, which in my case is MetroEntities.

·     In the InitializeService function, insert these three lines:

config.SetEntitySetAccessRule("*", EntitySetRights.AllRead);

config.SetServiceOperationAccessRule("*", ServiceOperationRights.All);

config.UseVerboseErrors = true;

·     Run locally under the debugger and navigate to your XXX.svc page.  You should see something like this:

<service xml:base=" http://localhost:4704/MetroData.svc/">

  <workspace>

    <atom:title>Default</atom:title>

    <collection href="MEventSet">

      <atom:title>MEventSet</atom:title>

    </collection>

    <collection href="MUserSet">

      <atom:title>MUserSet</atom:title>

    </collection>

  </workspace>

</service>

That's an XML list of the tables in the schema that Visual Studio imported from the database.  Note that you don't need a live database connection to see this XML list.  You only needed the connection at the time you created the entity data model.  You will, of course, need a live connection to get to the database itself when it's time to use the service to read and update data.

·     Publish and test locally

·     Copy to shared host and test there as well

Error: At most one address per scheme

You may see this error message:

This collection already contains an address with scheme http.  There can be at most one address per scheme in this collection.

Parameter name: item

This is a well-documented issue.  You need only change web.config to have:

<system.serviceModel>

  <serviceHostingEnvironment aspNetCompatibilityEnabled="true">

    <baseAddressPrefixFilters>

      <add prefix="http://www.your-domain-name.com"/>

    </baseAddressPrefixFilters>

  </serviceHostingEnvironment>

</system.serviceModel>

You may see other, more complicated solutions involving overriding the service host factory.  As far as I can tell, they are not necessary and the above solution will suffice.

 

Error: More than one authentication scheme

You might get this error running locally or on the shared host:

IIS specified authentication schemes 'Basic, Anonymous', but the binding only supports specification of exactly one authentication scheme.

The solution is to go into the IIS management window, open the properties for the virtual directory, go to Directory Security, edit the access and authentication control, and uncheck everything but anonymous access.  If it works locally but you get that message on the shared host, you'll have to ask the administrators of the shared host to make the same change.

Note: you'll have to restart the WWW publishing service for the change to take effect.

Error: 404

If you get a 404 error attempting to access your XXX.svc page, you need to check if .svc is a mapped extension. 

Go to the IIS management window, open the properties for the virtual directory, and edit the Application Configuration.  Check that .svc is mapped to aspnet_isapi.dll.  If it isn't, add the mapping.  Once again, if it works locally but gives a 404 on your shared host, you'll have to ask the administrators of the shared host to make the same change.  Of course a 404 could be anything, so you should carefully debug to make sure you're not crying wolf.

You may have to restart the WWW publishing service for the change to take effect.

As a convenience, create a web.changes.config file

When I test locally, I use a different connection string for the data service.  I also cannot have the baseAddressPrefixFilters.  So each time I copy the published web application to the shared host, I have to edit the web.config file.

To save time and avoid errors, I've created a small file that has the differences between the local configuration and the hosted configuration.


Create the Silverlight Client

We have now moved on to these MSDN notes. 

Add a new project

Right-click on the solution, choose Add new project, and choose Silverlight application. I named my new project MetroSilver.  On the next screen, I chose to link it to the existing web site.  I'm adding a test page, but not making it the start page since I'll later integrate Silverlight into a normal ASP.NET website.  I will enable Silverlight debugging.

Add a ListBox and a Button to supply dummy data

In the default.aspx of the web app, I put links to the testservice.svc and the silverlight test page (clienttest.aspx).

Then in page.xaml I added a simple listbox and a button, each in its own row in the grid.  Here's the code that goes inside the grid:

        <Grid.RowDefinitions>

            <RowDefinition MaxHeight="300" />

            <RowDefinition MaxHeight="100" />

        </Grid.RowDefinitions>

        <ListBox x:Name="dataSvcListBox"

            HorizontalAlignment="Stretch"

            Grid.RowSpan="1"

            Grid.Row="0" VerticalAlignment="Stretch"

            ItemsSource="{Binding Mode=OneWay}" >

        </ListBox>

        <Button x:Name="GetDataButton" Grid.Row="1" Height="20" Width="100" Content="Get Data" Click="GetDataButton_Click"></Button>

Then you need to add this function to page.xaml.cs:

        private void GetDataButton_Click(object sender, RoutedEventArgs e)

        {

           List<int> iList = new List<int> { 0, 1, 2, 3, 4, 5, 6 };

           dataSvcListBox.ItemsSource = iList;

        }

I published and tested locally and on the Infosaic host.  Pressing the button populates the list.  The new code works everywhere and none of the old code is broken.  At this point, there don't seem to be as many opportunities for difficult-to-trace errors.  Although…

Error: XAML Parsing error

It's worth noting that if you copied and pasted the XAML without inserting the click event handler, you wouldn't get a compiler error like you'd expect, since the XAML is compiled dynamically.  Instead you'll get a XAML parsing error with the misleading name of AG_E_PARSER_BAD_PROPERTY_VALUE. 

So naturally, you'll think something is wrong with the XAML syntax, when it's simply missing the code-behind event handler.

Add the Service Reference

Right click on the Silverlight project in Solution Explorer to Add a Service Reference.  Enter the address of the published service page (e.g. http://localhost/targetsoft/metrodata.svc) and click Go.

At this point, the data  service doesn't have to be running, but it has to be capable of running (so Visual Studio can run it on your behalf).  If for some reason, the services won't appear properly in the Visual Studio dialog, make sure you can run it outside Visual Studio in a browser on localhost.

Once the service runs it should list a tree of services.  You need to specify a namespace which will be nested within the client namespace.

In page.xaml.cs add:

using System.Data.Services.Client;

Implement the data client

Bind elements in the listbox to the columns returned by the data service

We'll redefine the listbox we added previously to page.xaml so it will bind to the fields in the MUser table in MetroEntities.  The table definitions for MetroEntities were picked up by the client when we added the service reference.  That's why the service had to be ready to run.

Here's the new listbox, bound to UserName, FirstName, and LastName:

        <ListBox x:Name="dataSvcListBox"

            HorizontalAlignment="Stretch"

            Grid.RowSpan="1"

            Grid.Row="0" VerticalAlignment="Stretch"

            ItemsSource="{Binding Mode=OneWay}" >

            <ListBox.ItemTemplate>

                <DataTemplate>

                    <StackPanel x:Name="DisplayListData"

                     Orientation="Horizontal"

                     VerticalAlignment="Bottom"

                     Margin="5" >

                        <TextBlock x:Name="UserName"

                         Text="{Binding UserName}"

                         Margin="5,0,0,0"

                         VerticalAlignment="Bottom"

                         HorizontalAlignment="Left"

                         FontSize="12">

                        </TextBlock>

 

                        <TextBlock x:Name="FirstName"

                         Text="{Binding FirstName}"

                         Margin="5,0,0,0"

                         VerticalAlignment="Bottom"

                         HorizontalAlignment="Left"

                         FontSize="12">

                        </TextBlock>

 

                        <TextBlock x:Name="LastName"

                         Text="{Binding LastName}"

                         Margin="5,0,0,0"

                         VerticalAlignment="Bottom"

                         HorizontalAlignment="Left"

                         FontSize="12">

                        </TextBlock>

                    </StackPanel>

                </DataTemplate>

            </ListBox.ItemTemplate>

        </ListBox>

The new code is contained by the ItemTemplate.


Retrieve the data from the service

In this example, I'll do all the work inside page.xaml/.cs.  In a real app, we'd have dedicated classes for the data instead of mixing them into the one UI class.

First, add a using for the client's data service.  In my case, it's:

using MetroSilver.targetsoft;

This was  the auto-generated class created when the service reference was added.

Now we'll replace the dummy code in GetDataButton_Click with live data retrieval code.  Since the retrieval takes place asynchronously, we need a function to request the data and a callback function to receive it.  Like this:

        private void GetDataButton_Click(object sender, RoutedEventArgs e)

        {

           try

           {

               DataServiceContext svcCtx = new DataServiceContext(

                   new Uri("MetroData.svc", UriKind.Relative));

 

               svcCtx.BeginExecute<MUser>(

                   new Uri("MUserSet", UriKind.Relative),

                   loadUserCallback, svcCtx);

           }

           catch (Exception ex)

           {

               throw ex;

           }

        }

        private void loadUserCallback(IAsyncResult asyncResult)

        {

           try

           {

               DataServiceContext ctx = asyncResult.AsyncState as DataServiceContext;

               IEnumerable<MUser> source = ctx.EndExecute<MUser>(asyncResult);

               dataSvcListBox.DataContext = source;

           }

           catch (Exception ex)

           {

               throw ex;

           }

        }

 


 But again, what if it doesn't work?

Though it did work for me in this case, (mainly because I had to slog through a bunch of errors earlier), I deliberately deleted the port # in the IP address of the Data Source, simulating the most likely error – a bad connection string.  If this happens, you won't get an error screen with an explanatory message.  You'll get no response at all.

To debug locally, pretending I didn't know the cause, I set a breakpoint and walked through the above code to find out it threw an exception in loadUserCallback.  The exception is:

An error occurred while processing this request.

Well, we knew that.  Checking the inner exception (which is a bit tedious in the debugger, but not too bad), I got:

An error occurred while saving changes. See the inner exception for details.

Again, not real helpful.  I dug deeper still, which is now getting tedious, and finally got to the meat:

<?xml version="1.0" encoding="utf-8" standalone="yes"?>

               … blah blah blah ..

<message>The underlying provider failed on Open.</message>

               … and farther down…

<message>A network-related or instance-specific error occurred while establishing a connection to SQL Server. The server was not found or was not accessible. Verify that the instance name is correct and that SQL Server is configured to allow remote connections. (provider: TCP Provider, error: 0 - A connection attempt failed because the connected party did not properly respond after a period of time, or established connection failed because connected host has failed to respond.)</message>

It couldn't connect because it couldn't find the server identified in the connection string.  Either the database is not running or the string is wrong (which it was).

The lesson here is if we can't connect to the database, Silverlight won't blast it onto a web page in front of us.  That's good for the user, no ugly error screens, but how do we debug outside the debugger?

I'm sure there are better ways than what I'm about to tell you, but the following works for me.

Write a log file that traces exceptions

I've attached the log file software as a separate download (it's a single .cs file) and it includes a function like the one used during database testing, to unwind exception messages.  It writes a small circular file  that wraps around on itself when it reaches its allocation limit.  The file appears on the client in the local settings directory. 

Service Activation Exception

Here's another error you might see:

[ServiceActivationException]: The service '/DataService/DataModel.svc' cannot be activated due to an exception during compilation.  The exception message is: Index was out of range. Must be non-negative and less than the size of the collection.

The cause was simple enough – the service wasn't working because I'd forgotten to reset the web.config for local operation (it was set for operation on the shared host).  This is the kind of misleading message that can fool you.

Anyway, at this point I have the code working under the debugger, locally without the debugger, and on the shared host at Infosaic.

It's time to move onto updating records.