Recently I was having major problems designing a smart client application because the architecture necessitated a n-tier (or multi tier) paradigm. This "Architecture" was non-negotiable insofar as the smart client being for a relatively successful business. The advantages of this type of architecture are well documented so feel free to Google search on the subject.
The following tutorial will take you through the steps required to create an n-tier application using Visual Studio 2008.
- Create a new windows application called NorthwindTraders
- In the same solution, add a new class library called Northwind.DataAccessLayer (Delete the Class1.cs file in this project)
- Add another class library called Northwind.BusinessEntities (Delete the Class1.cs file in this project)
- Add a WCF Service Application called WcfService1 (the default). Make sure you choose the WCF service under the web options shown below
Your solution explorer should have the following four projects.
In the DataAccessLayer project add a DataSet. To do this, make sure the DataAccessLayer project is selected in Solution Explorer, and from the main menu choose "add new data source". Make sure Database is selected and click “Next”.
Note: I’m assuming you have downloaded and installed the Northwind database, if not you can get the ubiquitous database here.
If you don’t already have a connection to Northwind, click the “New Connection” button and select the SQL database file
You should now be able to browse to the database, and test to make sure the connection is working
Click on “Next”, if you get the following dialog, saying yes makes a copy of the Northwind Database to your project. I usually say no because you ens up with dozens if not hundreds of copies of the same database in your Visual Studio Projects folders, this was you always use the same database in all your projects.
Save the connection as “NorthwindConnectionString” and Click “Next”. Choose the “Customers” and “Orders” tables, and Change the name of the DataSet to “CustomersDataSet” and click on “Finish”.
”Choose the default database option and click next. Select the Northwind database and then choose the customers and orders tables. Change the name of the DataSet to "CustomerOrdersDataSet" and click finish.
Double click the “CustomersDataSet” in Solution Explorer and right click on the dataset and select properties
In the properties pane you can now choose the DataSet project for the data access layer. This is a new feature in Visual Studio 2008 that allows you to separate you data access logic and your business logic into two projects, which was a real pain point in Visual Studio 2005. In this case choose the Northwind.BusinessEntities project and make sure you click “Save”.
If you double click the orders table in the DataSet designer, you should be taken into the business entities project, where you can add some validation logic.
namespace Northwind.BusinessEntities {
public partial class CustomersDataSet {
partial class OrdersDataTable
{
public override void EndInit()
{
base.EndInit();
this.ColumnChanged += new System.Data.DataColumnChangeEventHandler(OrdersDataTable_ColumnChanged);
}
void OrdersDataTable_ColumnChanged(object sender, System.Data.DataColumnChangeEventArgs e)
{
if (e.Column.ColumnName == this.OrderDateColumn.ColumnName
|| e.Column.ColumnName == this.ShippedDateColumn.ColumnName)
{
VaidateDates((OrdersRow)e.Row);
}
}
/// <summary>
/// Simple validation to ensure that the date an order is shipped is not
/// before it has been ordered
/// </summary>
/// <param name="ordersRow"></param>
private void VaidateDates(OrdersRow ordersRow)
{
if (ordersRow.OrderDate > ordersRow.ShippedDate)
{
ordersRow.SetColumnError(this.OrderDateColumn, "Cannot ship before it’s been ordered");
ordersRow.SetColumnError(this.ShippedDateColumn, "Cannot ship before it’s been ordered");
}
else
{
ordersRow.SetColumnError(this.OrderDateColumn, "");
ordersRow.SetColumnError(this.ShippedDateColumn, "");
}
}
}
}
}
We can now complete the setup by adding a class into the data access layer called the NorthwindDataManager that connects to the database, and returns the tables. The NorthwindManager will typically contain all the queries that you create. If you need to know how to create queries you can have a look at this article, or do some research. You typically you change the query to GetOrders and not GetData, but some knowledge of this is assumed.
This is the code that returns a DataTable in the NorthwindDataManager. Make sure you include the Using statements at the top
using System;
using System.Collections.Generic;
using System.Linq;
using Northwind.BusinessEntities;
using System.Text;
using Northwind.DataAccessLayer.CustomersDataSetTableAdapters;
namespace Northwind.DataAccessLayer
{
/// <summary>
/// Typically this class would not return all the data, but specific queries related to a table.
/// For the sake of brevity, we will return all the orders in the northwind database. Also try
/// change the GetData() to GetOrders() in the OrdersTableAdapter by modifying the query in the
/// DataSetDesigner. Knowledge of doing this is assumed.
/// </summary>
public class NorthwindDataManager
{
public static CustomersDataSet.OrdersDataTable GetOrders()
{
OrdersTableAdapter ordersAdapter = new OrdersTableAdapter();
return ordersAdapter.GetData();
}
}
}
Now we will configure the service. Select the WCF service project, right click the “References” node in Solution Explorer, and go “Add References.” choose the projects tab at the top and add the DataAccessLayer and BusinessEntities projects.
You also need to add a reference to System.Data.DataSetExtensions so add a reference as above, but this time choose the .NET tab
So your solution explorer should look like this
In the IService interface file, remove the default entries and add a contract called GetOrders (not forgetting the namespaces at the top)
using System;
using System.Collections.Generic;
using System.Linq;
using System.Runtime.Serialization;
using System.ServiceModel;
using System.Text;
using Northwind.BusinessEntities;
namespace WcfService1
{
// NOTE: If you change the interface name "IService1" here, you must also update the reference to "IService1" in Web.config.
[ServiceContract]
public interface IService1
{
[OperationContract]
CustomersDataSet.OrdersDataTable GetOrders();
}
}
In the Service1.scv file remove the default data and implement the IServive1 interface. A quick shortcut to do this is to right click the IService1 in Visual Studio shown below (this can save quite a lot of typing when setting up big service layers)
Add the following code (again note the namespaces above)
using System;
using System.Collections.Generic;
using System.Linq;
using System.Runtime.Serialization;
using System.ServiceModel;
using System.Text;
using Northwind.DataAccessLayer;
using Northwind.BusinessEntities;
namespace WcfService1
{
// NOTE: If you change the class name "Service1" here, you must also update the reference to "Service1" in Web.config and in the associated .svc file.
public class Service1 : IService1
{
#region IService1 Members
public CustomersDataSet.OrdersDataTable GetOrders()
{
return NorthwindDataManager.GetOrders();
}
#endregion
}
}
To complete the tutorial go to the NorthwindTraders project, which typically is your client application and make sure you add a reference to the business entities project. This is important because it allows proxy reuse (another VS 2008 Key feature). You should also see here that at no stage is the data access component ever available or referenced on the client. Make sure you build the entire Solution now by clicking F5.
Right click the references node and add a service reference this time.
Click the discover button and wait till the reference is located and then click OK
Note: A word of warning is that if you use third party software libraries, than you do not want their libraries included here. Be sure to click the advanced button, and choose “Reuse types in specified referenced assemblies” and omit them. Failure to do this may result in some rather nasty Visual Studio crashes.
In the app.config, as a security precaution, the client configuration we generate has a default MaxRecievedMessageSize value of 65536, meaning that an exception will be thrown at runtime if the service sends more data to the client than it expects. Since we know we will be passing large DataSets over the wire, please open up the app.config and set the element <bindings>\<wsHttpBinding>\<binding>’s maxReceivedMessageSize attribute to 5000000. Or, if you feel uncomfortable editing your service’s configuration by hand, you can open up the Microsoft Service Configuration Editor available under Visual Studio’s Tools menu.
<?xml version="1.0" encoding="utf-8" ?>
<configuration>
<system.serviceModel>
<bindings>
<wsHttpBinding>
<binding name="WSHttpBinding_IService1" closeTimeout="00:01:00"
openTimeout="00:01:00" receiveTimeout="00:10:00" sendTimeout="00:01:00"
bypassProxyOnLocal="false" transactionFlow="false" hostNameComparisonMode="StrongWildcard"
maxBufferPoolSize="524288" maxReceivedMessageSize="5000000"
messageEncoding="Text" textEncoding="utf-8" useDefaultWebProxy="true"
allowCookies="false">
<readerQuotas maxDepth="32" maxStringContentLength="8192" maxArrayLength="16384"
maxBytesPerRead="4096" maxNameTableCharCount="16384" />
<reliableSession ordered="true" inactivityTimeout="00:10:00"
enabled="false" />
<security mode="Message">
<transport clientCredentialType="Windows" proxyCredentialType="None"
realm="" />
<message clientCredentialType="Windows" negotiateServiceCredential="true"
algorithmSuite="Default" establishSecurityContext="true" />
</security>
</binding>
</wsHttpBinding>
</bindings>
<client>
<endpoint address="http://localhost:2429/Service1.svc" binding="wsHttpBinding"
bindingConfiguration="WSHttpBinding_IService1" contract="ServiceReference1.IService1"
name="WSHttpBinding_IService1">
<identity>
<dns value="localhost" />
</identity>
</endpoint>
</client>
</system.serviceModel>
</configuration>
Now that our service has been added and configured correctly, let’s do some data binding to the data tables returned from our service. Open up the Data Sources window and drag and drop the Orders table onto Form1.
Finally, the last thing we need to do is add code to Form1’s OnLoad event to get data from our WCF service.
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Data;
using System.Drawing;
using System.Linq;
using System.Text;
using System.Windows.Forms;
namespace NorthwindTraders
{
public partial class Form1 : Form
{
private ServiceReference1.Service1Client serviceClient;
public Form1()
{
InitializeComponent();
this.serviceClient = new NorthwindTraders.ServiceReference1.Service1Client();
}
private void Form1_Load(object sender, EventArgs e)
{
this.customersDataSet.Merge(this.serviceClient.GetOrders());
}
// Being a good citizen, always close a service connection
private void Form1_FormClosing(object sender, FormClosingEventArgs e)
{
this.serviceClient.Close();
}
}
}
As you can see when the form is loaded and the dates changed, the validation occurs in a library separate from the client.
Really useful, thanks!
Pingback: UserControl closing event « Castalian
There’s something I do not understand. Why is it that in your client application you add a reference to the BusinessEntities project? I would have thought that from the client application the only access would have been through your WCF service and its methods.
Hello Rod,
The reason the BusinessEntities.dll is referenced is due to a great feature in Visual Studio 2008 in which the Proxy Types are re-used.
The example I have above is based on a Visual Basic demo by John Stallo at http://blogs.msdn.com/vbteam/archive/2007/08/30/A-Walkthrough-of-WCF-Support-in-Visual-Studio-2008.aspx
Please read the proxy re-use section. You can also watch the associated video at http://channel9.msdn.com/posts/funkyonex/Building-N-Tier-Applications-in-Visual-Studio-2008/ if you need to but it is in VB
cheers,
Ira
Hi Ira,
Thank you for the reply! I appreciate it.
I’ve got another question for you. I’m up where I’ve decided the GetData() method of the NorthwindDataManager class, and I’m getting the following error from VS 2008:
“The type or namespace name ‘CustomerOrdersDataSet’ could not be found (are you missing a using directive or an assembly reference?)”
I don’t believe I’ve left any steps out, so I’m not sure why I’m getting this error.
You need to make sure you add a reference to System.Data.DataEetExtensions and also make sure you have the correct using statements at the top.
using Northwind.BusinessEntities;
using Northwind.DataAccessLayer.CustomerOrdersDataSetTableAdapters;
Also make sure that it is a .NET 3.5 project, and that you have not created a .NET 2.0 project.
In will try and find the demo I made and post that as well, because I can now see the benefit of posting the code sample as well. That way people can have a look to see that it works, or if they have left something out.
Ira, I appreciate very much your help, and yes the demo code would likely help me as well. I’ve checked my using statements and I have both the Nortwind.BusinessEntities and Northwind.DataAccessLayer.CustomerOrdersDataSetTableAdapters assemblies. I’m also targetting the .NET 3.5 framework. But I’m still getting errors. When I get to the point of attempting to add a service reference to the WCF service I cannot expand the Service1.svc service. I get a lengthly error code, which I’ll try and reduce to the relevant portion here:
“The type ‘WcfService1.Service1’, provided as the Service attribute value in the ServiceHost directive could not be found. … The type ‘WcfService1.Service1’, provided as the Service attribute value in the ServiceHost directive could not be found.”
OK, my fault, my bad, my mistake, etc.
I’ve just discovered that I had made a small typo in the NorthwindDataManager.cs file. I had:
public static CustomerOrdersDataSet.OrdersDataTable GetData() { … }
whereas I should have had:
public static CustomerOrdersDataset.OrdersDataTable GetData() { … }
Please note the capitalization in “…DataSet”. I is my fault for having messed this up, I’m sorry.
Well, I’ve gotten this to work. Yeah!!
Now I’ve got to go back to an earlier question, to broaden its scope. In this example the WCF service, and the data access and business logic projects are all a part of the same solution as is the client application. But that’s not how I develop my applications. I tend to write my WCF applications as separate applications which will be used by my client application. And in some cases my WCF services are used by more than one client application, so including it in solution with a client application isn’t practical. I have seen, through your example here, that separating the data access and business logic layers into their own separate projects has benefits. But, at least in my limited experience with WCF, the only things that have access to WCF services from outside of the WCF project, and not a part of the same solution, are those methods in the WCF’s interface decorated with the [ServiceContract] and [OperationContract] and other such attrbutes. Having access to the BusinessEntities class from the client application, isn’t possible (I think) since they’ll both be in different solutions.
Or am I completely wrong about this?
I’ve got a question for clarification. When creating hte NorthwindDataManager class, in your case above all you did was define a GetData() method to retrieve just the orders data. But you mentioned that you did this for brevity, and that if it had been fully developed you would return all of the data that all of the queries would return.
What I want to know is, does this mean you would have created another GetData() method to return the Customers data (in your example)? The WCF service that I’m working on, based upon your example here, will return data from roughly 30 tables in 2 different databases. I want to know if I’ve got to create 30 different GetData() methods (which I assume I do), or if your idea was that there would be one GetData() method which would return all of the different data tables as arguments to the single GetData() method?
Hi Rod,
I will update this post tomorrow with how I handle this scenario.
Thanks
Ira
Pingback: choosing the correct Database access technology for your application « Castalian
Pingback: UserControl closing event « Ira Lukhezo
Hi Ira.
Thank you for this tutorial.
I got some error during debugging. hier is the error message
The server was unable to process the request due to an internal error. For more information about the error, either turn on IncludeExceptionDetailInFaults (either from ServiceBehaviorAttribute or from the configuration behavior) on the server in order to send the exception information back to the client, or turn on tracing as per the Microsoft .NET Framework 3.0 SDK documentation and inspect the server trace logs.
Hi kemson78@yahoo.de,
I have not come across this error message before. I have posted the code sample for this project at NorthwindTraders
I have just tried to run this program and it works so please try it.
If it works then there is a problem with your setup, but if it does not, your configuration will be awry. Are you using Visual Studio 2008, on XP or Vista, and is IIS enabled on the machine?