Introduction
I am sure there is a lot discussion about this topic and many people writing about it, however I find the most of the examples too complex or too simple like Hello World. So, here is my take on this from a simplistic point of view.
Problem
Silverlight is a very good platform which truly blends as a rich web experience application platform; however the problem is integration with data using web-services, now that can be a bit challenging for people like me. Here I am trying to demonstrate how to implement custom Lists
to achieve the same data-binding capabilities as that of a conventional DataSet
web services.
Pre-requisite
Understanding of Web Services in general and System.Collections.Generic.Lists
; when I say Web Services I mean creating and hosting of standard ASP.NET web services using Visual Studio. Here is a nice article Creating and consuming a Web Service using Visual Studio .NET on how to create Web Services
Understanding of creating Silverligh Applications,WPF and Microsoft Expression Blend, optionally you can get the Silverlight ToolKit SDK.
Why we need System.Collections.Generic.Lists
, is because SilverLight Applications do not understand System.Data.DataSet
objects and there is a very good reason of not supporting this. Instead of System.Data.DataSet
you can bind SilverLight Objects to a System.Collections
which is much organized, strongled typed and can implemented additional element level protection.
Patron
I am sure my company’s security team will be raising their ears / eye and all possible things when they see “Patron” on the web, and again there is a good reason as this is the core business object. However, what I am trying to establish here is to give a background on few of the data elements that we can look at to see how we can write Silverlight web service and at the same time protect individual sensitive data based on the request; like element masking.
Patron Master |
Data Column |
Column Name |
Data Type |
CompanyCode |
Primary Property |
character |
PatronID |
Patron Number. |
character |
PatronName |
Patron Name. |
character |
|
|
Transaction |
Data Column |
Column Name |
Data Type |
ChipPurchaseVoucher |
Chip Purchase Voucher |
character |
CPVSequence |
CPV Sequence |
integer |
GameDate |
Gaming Date. |
date |
PAccount |
Patron Account. |
character |
PAccBal |
Patron Account Balance. |
decimal |
DocumentAmount |
Document Amount. |
decimal |
BalanceDue |
Balance Due. |
decimal |
CompanyCode |
Company Code. |
character |
|
Contract(s)
As Silverlight does not understand System.Data.DataSet
we have to define Data Contracts for all our data that will be passed back to the Silverlight Application. If you are not keen on diving deep into the ServiceContract / OperationContract / DataContract then on a very simplistic view you can look at DataContract as a Agreed Data Exchange Contract between Consumer & Publisher for strong data binding; on the other hand ServiceContract is the Entry Point into your service, typically called as EndPointAddress (strange but true, as this is the place where the consumer request for Operations), and that is what is OperationsContract or the Method()
the consumer will call.
DataContract - Patron
So, lets define a class and mark it with the System.Runtime.Serialization.DataContract
class.
namespace SilverLightWS
{
[DataContract]
public class TPatron
{
[DataMember(Name="Company Code", Order=1)]
public String CompanyCode { get; set; }
[DataMember(Name="Patron ID", Order=2)]
public String PatronID { get; set; }
[DataMember(Name="Patron Name", Order=3)]
public String PatronName { get; set; }
}
}
DataContract - Transaction
namespace SilverLightWS
{
[DataContract]
public class TTransaction
{
[DataMember(Name="Company Code", Order=1)]
public String CompanyCode { get; set; }
[DataMember(Name="Patron ID", Order=2)]
public String PatronID { get; set; }
[DataMember(Name="Patron Name", Order=3)]
public String PatronName { get; set; }
[DataMember(Name="Chip Purchase Voucher", Order=4)]
public String ChipPurchaseVoucher { get; set; }
[DataMember(Name="CPV Sequence", Order=5)]
public int CPVSequence { get; set; }
[DataMember(Name="Gaming Date", Order=6)]
public DateTime GameDate { get; set; }
[DataMember(Name="Patron Account", Order=7)]
public String PAccount { get; set; }
[DataMember(Name="Patron Account", Order=8)]
public Currency PAccBal { get; set; }
[DataMember(Name="Total Amount", Order=9)]
public Currency DocumentAmount { get; set; }
[DataMember(Name="Balance Due", Order=10)]
public Currency BalanceDue { get; set; }
public TTransaction()
{
this.PatronName = "****";
this.PatronID = "****";
}
}
}
ServiceContract - getCasinoDocuments()
Let's define the web method that will be used by SilverLight client side to consume.
namespace CasinoWebService
{
[ServiceContract(Namespace = "")]
[AspNetCompatibilityRequirements(RequirementsMode = AspNetCompatibilityRequirementsMode.Allowed)]
public class CasinoDocuments
{
private DataSet dsCasinoDocuments;
private OdbcConnection oCMConnection;
private OdbcDataAdapter daCasinoDocuments;
[OperationContract]
public List<TTransaction> getCasinoDocuments(String CompanyCode, DateTime StartDate, DateTime EndDate)
{
try
{
String SelectStatement = "SELECT * FROM MyDataTable"
oCMConnection = new OdbcConnection();
oCMConnection.ConnectionString = ConfigurationManager.ConnectionStrings[ConnectionName].ConnectionString;
oCMConnection.Open();
daCasinoDocuments = new OdbcDataAdapter(SelectStatement, oCMConnection);
dsCasinoDocuments = new DataSet();
daCasinoDocuments.Fill(dsCasinoDocuments, "vwtable");
oCMConnection.Close();
var CMDocs = new List<TTransaction>();
foreach (DataRow r in ds.Tables[0].Rows)
{
TTransaction oRow = new TTransaction();
oRow.CompanyCode = r["CompanyCode"].ToString();
if (HttpContext.Current.User.IsInRole(Environment.MachineName + "\\AllowedAccess"))
{
oRow.PatronID = r["PatronID"].ToString();
DataSet dsPatron = oService.getPatron(oRow.CompanyCode, oRow.PatronID);
if (dsPatron.Tables[0].Rows.Count > 0) oRow.PatronName = dsPatron.Tables[0].Rows[0]["PatronName"].ToString();
}
oRow.ChipPurchaseVoucher = r["ChipPurchaseVoucher"].ToString();
oRow.CPVSequence = Convert.ToInt(r["CPVSequence"].ToString());
oRow.GameDate = Convert.ToDateTime(r["GameDate"].ToString());
oRow.PAccount = r["PAccount"].ToString();
oRow.PAccBal = Convert.ToDecimal(r["PAccBal"].ToString());
oRow.DocumentAmount = Convert.ToDecimal(r["DocumentAmount"].ToString());
oRow.BalanceDue = Convert.ToDecimal(r["BalanceDue"].ToString());
CMDocs.Add(oRow);
}
return CMDocs;
}
catch (Exception e)
{
throw e;
}
}
}
This is pretty much for the Web Service Side; if you want to host this on a standard IIS environment then go ahead and publish it, Visual Studio will put all the necessary standard serviceModel behaviours and bindings in the Web.config. However you have to understand the System.serviceModel
if you are hosting it in complex environment where the clients are behind Proxy or FireWalls etc.
<system.serviceModel>
<behaviors>
<serviceBehaviors>
<behavior name="CasinoWebService.DocumentsBehavior">
<serviceMetadata httpGetEnabled="true" />
<serviceDebug includeExceptionDetailInFaults="true" />
</behavior>
</serviceBehaviors>
</behaviors>
<bindings>
<customBinding>
<binding name="customBinding0">
<binaryMessageEncoding />
<httpTransport />
</binding>
</customBinding>
</bindings>
<serviceHostingEnvironment aspNetCompatibilityEnabled="true" />
<services>
<service behaviorConfiguration="CasinoWebService.DocumentsBehavior"
name="CasinoWebService.Documents">
<endpoint address="" binding="customBinding" bindingConfiguration="customBinding0" contract="CasinoWebService.Documents" />
<endpoint address="mex" binding="mexHttpBinding" contract="IMetadataExchange" />
</service>
</services>
</system.serviceModel>
Silverlight Client
Create a new Silverlight Application Project in VS (I strongly recommend get Expression Blend), right click on the project "Add Service References"; enter the URL where you have published the web service and add the WebReference. Here is a nice article on how to...; VS will create the ServiceReferences.ClientConfig file.
We will keep the default System.Collections.Generic.Dictionary
for the data type; as we will be returning List<T>
ServiceReferences.ClientConfig
<configuration>
<system.serviceModel>
<bindings>
<customBinding>
<binding name="CustomBinding_Documents">
<binaryMessageEncoding />
<httpTransport maxReceivedMessageSize="2147483647" maxBufferSize="2147483647" />
</binding>
</customBinding>
</bindings>
<client>
-->
<endpoint address="http://localhost/POC/Documents.svc"
binding="customBinding" bindingConfiguration="CustomBinding_Documents"
contract="ServiceReference1.Documents" name="CustomBinding_Documents" />
</client>
</system.serviceModel>
</configuration>
Putting it together
Following is the XAML for the MainPage.XAML, this is the user control that is created by VS as default; bind the DataContext
of DataGrid
to the DataContract as defined by the service.
<UserControl
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" xmlns:data="clr-namespace:System.Windows.Controls;assembly=System.Windows.Controls.Data"
xmlns:CasinoWebService_wssSycoSL="clr-namespace:CasinoWebService.wssSycoSL"
xmlns:controlsToolkit="clr-namespace:System.Windows.Controls;assembly=System.Windows.Controls.Toolkit"
xmlns:expressionDark="clr-namespace:System.Windows.Controls.Theming;assembly=System.Windows.Controls.Theming.ExpressionDark"
xmlns:navigation="clr-namespace:System.Windows.Controls;assembly=System.Windows.Controls.Navigation"
xmlns:local="clr-namespace:CasinoWebService"
x:Class="CasinoWebService.MainPage" Height="672" Width="1128">
<Grid x:Name="LayoutRoot">
<Grid x:Name="grdSearch" Background="#FF9DB4F2" Height="136" VerticalAlignment="Top" HorizontalAlignment="Left" Width="328" Margin="8,8,0,0" >
<TextBlock x:Name="lblCompanyCode" Text="Company Code" Height="24" VerticalAlignment="Top" HorizontalAlignment="Left" Width="88" Margin="8,8,0,0" />
<TextBox x:Name="txtCompanyCode" Height="24" VerticalAlignment="Top" HorizontalAlignment="Left" Width="96" Margin="112,8,0,0" Text="502"/>
<TextBox x:Name="txtStartDate" Height="24" VerticalAlignment="Top" Margin="112,36,0,40" HorizontalAlignment="Left" Width="96" Text="11/9/2009" />
<TextBox x:Name="txtEndDate" HorizontalAlignment="Left" Width="96" Margin="112,64,0,45" Text="11/9/2009" d:LayoutOverrides="VerticalAlignment"/>
<Button x:Name="btnView" Content="View" Click="btnView_Click" HorizontalAlignment="Left" Width="96" Margin="112,0,0,8" Height="33" VerticalAlignment="Bottom" d:LayoutOverrides="VerticalAlignment" />
<Button x:Name="btnSave" Content="Save" Click="btnSave_Click" Height="33" HorizontalAlignment="Left" VerticalAlignment="Bottom" Width="96" Margin="212,0,0,8" d:LayoutOverrides="VerticalAlignment" />
<TextBlock x:Name="lblStartDate" Text="Start Date" HorizontalAlignment="Left" Width="88" Margin="8,36,0,84" FontSize="10.667"/>
<TextBlock x:Name="lblEndDate" Text="End Date" HorizontalAlignment="Left" Width="88" Margin="8,64,0,45" FontSize="10.667" d:LayoutOverrides="VerticalAlignment"/>
</Grid>
<Grid x:Name="grdData" Margin="0,148,0,0" Height="244" VerticalAlignment="Top" d:LayoutOverrides="VerticalAlignment">
<data:DataGrid x:Name="dgView" ItemsSource="{Binding Mode=OneWay}" FrozenColumnCount="2" Margin="8" RenderTransformOrigin="0.5,0.5" d:LayoutOverrides="VerticalAlignment, GridBox">
<data:DataGrid.DataContext>
<CasinoWebService_wssSycoSL:TTransaction/>
</data:DataGrid.DataContext>
</data:DataGrid>
</Grid>
<controlsToolkit:BusyIndicator x:Name="cntrlBussy" Margin="384,40,392,0" Content="" BusyContent="Loading..." Cursor="Wait" d:LayoutOverrides="VerticalAlignment, GridBox" Height="72" VerticalAlignment="Top"/>
<Canvas x:Name="cnvObject" Height="200" VerticalAlignment="Bottom" Margin="8,0,8,8">
<Border x:Name="cnvBorder" BorderBrush="#FFC16161" CornerRadius="5" BorderThickness="3" Height="200" Width="1112">
<Border.Background>
<LinearGradientBrush EndPoint="0.5,1" StartPoint="0.5,0">
<GradientStop Color="#FF598194" Offset="0.165"/>
<GradientStop Color="#FF303234" Offset="0.961"/>
<GradientStop Color="#FF4A6A71" Offset="0.448"/>
</LinearGradientBrush>
</Border.Background>
</Border>
</Canvas>
<Button x:Name="btnAdd" Height="32" Margin="480,0,552,232" VerticalAlignment="Bottom" Content="Add" Click="btnAdd_Clicked" d:LayoutOverrides="VerticalAlignment"/>
</Grid>
</UserControl>
This is the code-behind of the MainPage User Control.
namespace CasinoWebService
{
public partial class MainPage : UserControl
{
CasinoWebServiceClient wsProxy = new CasinoWebServiceClient();
public MainPage()
{
InitializeComponent();
}
private void btnView_Click(object sender, System.Windows.RoutedEventArgs e)
{
try
{
wsProxy.getCasinoDocumentsCompleted += new EventHandler(wsProxy_getCasinoDocumentsCompleted);
cntrlBussy.IsBusy = true;
wsProxy.getCasinoDocumentsAsync(txtCompanyCode.Text, getDateTime(txtStartDate.Text), getDateTime(txtEndDate.Text));
}
catch (Exception ex)
{
throw ex;
}
}
private void btnSave_Click(object sender, System.Windows.RoutedEventArgs e)
{
try
{
MessageBox.Show("Feature not implemented", "POC", MessageBoxButton.OK);
}
catch (Exception ex)
{
throw ex;
}
}
private void btnAdd_Clicked(object sender, System.Windows.RoutedEventArgs e)
{
try
{
double left = 0, width = 0;
foreach (object oCarrousel in this.cnvObject.Children)
{
if (oCarrousel.GetType().FullName == "CasinoWebService.PatronCarrousel")
{
PatronCarrousel oItem = (PatronCarrousel)oCarrousel;
width = (double)oItem.GetValue(Canvas.ActualWidthProperty);
left += (double)oItem.GetValue(Canvas.ActualWidthProperty);
}
}
if ((left +width) < (double)this.cnvObject.GetValue(Canvas.ActualWidthProperty))
{
this.cnvObject.Children.Add(new PatronCarrousel((dgView.SelectedItem as TTransaction)));
Canvas.SetLeft(this.cnvObject.Children[this.cnvObject.Children.Count - 1], left);
}
}
catch (Exception)
{
MessageBox.Show("Select item from the datagrid.", "No Item", MessageBoxButton.OK);
}
}
void wsProxy_getCasinoDocumentsCompleted(object sender, getCasinoDocumentsCompletedEventArgs e)
{
dgView.DataContext = e.Result;
dgView.Visibility = Visibility.Visible;
cntrlBussy.IsBusy = false;
wsProxy.getCasinoDocumentsCompleted -= new EventHandler(wsProxy_getCasinoDocumentsCompleted);
}
private DateTime getDateTime(String inputDateTime)
{
if (inputDateTime == null || inputDateTime == "") return DateTime.Now;
DateTime retDate = Convert.ToDateTime(inputDateTime, new System.Globalization.CultureInfo("en-GB"));
return retDate;
}
}
}
Conclusion
This seems to be a lot of work, but on the other hand I believe that now with System.Runtime.Serialization.DataContract
we can truly make our
web services universal and easy to consume with any type of client.
On the other hand the Silverlight platform opens up huge potentials of a totally
new application development concepts without cranking Session(s) on IIS server to maintain state.
History