The way that we obtain our data from our web app.
This blog post is part three of my four part series on SEO for Silverlight Applications. I put this content together for my talk I gave at the Minnesota Developer's Conference which was last September. Fun Times. ( :-) )
The other posts in this series can be found here: Part I, Menu in Html and Silverlight, this is the Sitemap demo
Part II, Show Multiple Silverlight Controls in the Same Page with jQuery
The problem is how to transport objects and data from server to silverlight and back and do the least amount of typing. The databinding features of a Silverlight application make transporting the contents of an object from code to UI so simple. That's how I want to be able to send the contents of the object to the server and how I want to be able to retrieve the contents of that object on the server side.
In a monolithic web application this is simple, serialize from object to xml, do your transport, and serialize back to the object again.
In a silverlight project, it's not so simple for these reasons:
- Silverlight doesn't natively support serialization
- Silverlight objects in binary form must be in a separate runtime. (double the fun)
- You cannot share the same binary business class between the server app and the Silverlight app.- thus Each class in a Silverlight project must be echoed as a class on the server
One way to resolve these problems and add a service layer to your .aspx/mvc application is to use a code generator and run codegen for the xml views. Then when the Silverlight control requests data, redirect the request from an html based view to an xml based view. That’s how the addition I made to the MVC Store application is built.
There are a few aspects of this this blog post won’t cover today. When you use XML, you increase the size of your request object which slows down your silverlight app and pushes up against built in request limits. Normally, in a production application, to avoid these difficulties, I would add a compression /decompression step in the middle of the communication. This saves on bandwidth. Today’s demo doesn’t use compression – but you need to be aware of the need for it. Because Silverlight doesn’t natively support compression, you’ll need to take advantage of other add-in’s that do. (modification: 10/26/2008 - you can find a compression library in this control set: Silverlight Contrib)
Ok, let’s take a look at the parts of this program which are responsible for the communication. On the MVC application side, in global.asax – we’ve got a controller wired up for ajax style communications. This controller is the CatalogController and it has an Action inside that goes into play when a url beginning with ‘async’ is requested. This action is going to get the requested data from the database, and send it to an xml view for formatting.
In the xml view, there’s one thing to note here and that is the >xml opening tag is up on the top line to eliminate whitespace which causes errors in xml parsing in silverlight. But apparently there isn’t a way to eliminate enough of it so I do also trim the returned xml string from inside of Silverlight. – Before I load it into XDocument, I must trim it. So, anyway, let’s look at the Silverlight control and walk through how the product list is pushed out to the user. Appl.xaml.cs receives the incoming request to start the application.
if (!string.IsNullOrEmpty(controlid)){switch (controlid){case "1":this.RootVisual = new menu();break;case "2":try {string param = e.InitParams["requested"];this.RootVisual = new ProductDetails( param );}
It receives a controlid valued at “2”. When this code receives a #2 for controlid value in InitParams, This code reads the InitParams “requested” value. Requested InitParams value is a URL.
If one exists, the ProductDetails.xaml is then created with the value of the URL passed into the constructor.
In the constructor of ProductDetails,
- The constructor hooks up the page loaded event and sets a member var to the value received for requested into the _url member.
- In the pageload, if _url isn’t empty – then we call a function called initiateValuesLoad and this is where the request will be made back to the server for this object’s contents.
void initiateValuesLoad(){
try{string URIvalue = getBaseUrl();
string serviceUrl = URIvalue + "async" + _url;
WebClient client = new WebClient();
client.DownloadStringCompleted += new DownloadStringCompletedEventHandler(DownloadStringCompletedEvent);
client.DownloadStringAsync(new Uri(serviceUrl));
}
catch (Exception) { }
}
void DownloadStringCompletedEvent(object sender, DownloadStringCompletedEventArgs e)
{
if (e.Error == null){this.sitemapXML = e.Result;
if (e.Result.Length > 0)Dispatcher.BeginInvoke(delegate(){setProduct();
});}
Function initiateValuesLoad is just very simply a WebClient request. It’s asyncronous, so we set our DownloadStringCompletedEventHandler to a value of a function that will parse our received data. If we get data back. So in DownLoadStringCompleted, if the value of the DownloadStringCompletedEventArgs is not an error condition – the member variable called sitemapXML (a string var) is set to the Result of the request and we call function setProduct which is going to finish up here. Now, Dispatcher.BeginInvoke is used here to launch a thread within which the setProduct function will do it’s job. We need to use Dispatcher.BeginInvoke because we are not allowed to touch the UI from a background thread. We must get on this Special Plane of Existence which allows us to touch the UI.
private void setProduct()
{
sitemapXML = sitemapXML.Trim();
//removing spaces at the beginning, the spaces seem unavoidable with this approach.
XDocument xdoc = XDocument.Parse(sitemapXML);
_Cat = from productitem in xdoc.Descendants("productMapNode")
select new CatItem{
CatItemName = (string) productitem.Attribute("name"),
CatItemPageUri = (string) productitem.Attribute("pageurl"),
CatItemImage = (string) productitem.Attribute("imageurl"),
Productcode = (string) productitem.Attribute("productcode"),
Price = (string)productitem.Attribute("price")};
if (_Cat.Count() > 0)
{
fillWrappable();
//the object is ready to show itself - this is javascript call.
HtmlPage.Window.Invoke(" showProductList");}
}
In setProduct we trim our white space, parse our XDocument, load our IEnumerable as we saw in the Sitemap Menu example – same code that’s used in the menu loading a different business object class. Same XLinq. And in the end we go to our fillWrappable function which will create the XAML for our category list display.
What I’ve done here in this function is, I created a button template, which is XAML. That I’m going to load from a resource. I placed this XAML snippet into a resource because I wanted to have some experience of using a resource – it could just as easily have been a string variable within this function.
<button xmlns="http://schemas.microsoft.com/client/2007 xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml xmlns:vsm="clr-namespace:System.Windows;assembly=System.Windows" style="{2}" x:name="Cat{5}">
<button.template>
<controltemplate targettype="Button">
<grid>
<grid.background>
<lineargradientbrush endpoint="0.5,1" startpoint="0.5,0">
<gradientstop color="#FF000000" />
<gradientstop color="#FF0B123C" offset="1" />
</lineargradientbrush>
</grid.background>
<grid.rowdefinitions>
<rowdefinition height="*" />
<rowdefinition height="Auto" minheight="35.772" />
<rowdefinition height="Auto" minheight="74" />
</grid.rowdefinitions>
<grid.rendertransform>
<transformgroup>
<translatetransform x:name="MainButtonTranslate" x="0.0" y="0.0" />
<scaletransform x:name="MainButtonScale" scalex="1.0" scaley="1.0" />
</transformgroup>
</grid.rendertransform>
<vsm:visualstatemanager.visualstategroups>
<vsm:visualstategroup x:name="CommonStates">
<vsm:visualstategroup.transitions>
<vsm:visualtransition duration="0:0:0.5" to="MouseOver">
<storyboard>
<doubleanimation storyboard.targetname="MainButtonScale" storyboard.targetproperty="ScaleX" to="1.1" duration="0:0:0.05" />
<doubleanimation storyboard.targetname="MainButtonScale" storyboard.targetproperty="ScaleY" to="1.1" duration="0:0:0.05" />
</storyboard>
</vsm:visualtransition>
<vsm:visualtransition duration="0:0:0.5" to="Pressed">
<storyboard>
<doubleanimation storyboard.targetname="MainButtonTranslate" storyboard.targetproperty="X" to="1.1" duration="0:0:0.05" />
<doubleanimation storyboard.targetname="MainButtonTranslate" storyboard.targetproperty="Y" to="1.1" duration="0:0:0.05" />
</storyboard>
</vsm:visualtransition>
</vsm:visualstategroup.transitions>
<vsm:visualstate x:name="Normal" />
<vsm:visualstate x:name="MouseOver"></vsm:visualstate>
<vsm:visualstate x:name="Pressed"></vsm:visualstate>
<vsm:visualstate x:name="Disabled">
</vsm:visualstate>
</vsm:visualstategroup>
<vsm:visualstategroup x:name="FocusStates">
<vsm:visualstate x:name="Focused"></vsm:visualstate>
<vsm:visualstate x:name="Unfocused"></vsm:visualstate>
</vsm:visualstategroup>
</vsm:visualstatemanager.visualstategroups>
<image 'image{5}' Source='{1}' Style='{4}' Grid.Row='0' Margin="6,4,6,0" Grid.RowSpan="1" HorizontalAlignment="Center" VerticalAlignment="Top"/>
<textblock style="{3}" text="{0}" grid.row="1" verticalalignment="Bottom" foreground="#FFFFFFFF" x:name="title" />
<textblock horizontalalignment="Left" verticalalignment="Top" text="{6}" textwrapping="Wrap" x:name="Desc" grid.row="2" foreground="#FFFFFFFF" /></grid>
</controltemplate>
</button.template>
</button>
You can see here where the one button for each category item in the list, we take our button xaml string and we format it together with our product data and some style information using String.Format. The xaml control is then created using XamlReader.Load( onebutton )
The new button object receives it’s click event, And the button is appended to this Wrappable object which exists in the XAML and is going to present my buttons side by side then down.
private void fillWrappable(){
StringBuilder sb = new StringBuilder();
//I don't think you can use a Border in here when you're using a wrap panel. I couldn't take the time to see why though
byte[] bytes = ReadBytesFromStream("MDCSilverlightDemo.BL.buttontemplate.xml");
string buttonTemplate = "";
UTF8Encoding encoding = new UTF8Encoding();
buttonTemplate = encoding.GetString(bytes.ToArray(), 0, (int)bytes.Length);
int ictr = 0;
foreach (CatItem mnu in _Cat)
{
string onebutton = string.Format(buttonTemplate, mnu.CatItemName, mnu.CatItemImage," {StaticResource buttonStyle1}",
"{StaticResource CatItemNameBlock}", "{StaticResource ThumbNailPreview}",ictr.ToString());
ictr += 1;
Button bt = (Button)XamlReader.Load(onebutton);
sb.Append(onebutton).Append("\r\n");
bt.Click += new RoutedEventHandler(bt_Click);
Wrappable.Children.Add(bt);
}
string allbuttons = sb.ToString();
//debug. !!!}
So, basically – that’s it for our Service Layer Communciation.