Silverlight-Capture and Define Rectangles/Regions and Wire Events on them

by Bobbi Perreault 29. October 2008 12:46
Share on Facebook

This is a piece of an ecommerce tool I built, I call it the Room Builder. It was what I learned Silverlight on - so this project has been brought up from Silverlight 1.1 Alpha version. In other words, you may see some bad practices in there. But it's still useful. Or could be to the right situation.

Specifically today I want to talk about one of the features of the tool, that is the part that allows me to designate a region of a photograph, name it, and wire events to it for use in the application.

There is a link to the finished tool, so you can see what I mean on the left side of this blog, under the section titled "links to some of my silverlight controls".   If you hold your mouse over that link there should be a window pop up with instructions on it's use.

Regions of the photograph are targeted by clicking at key points along the perimeter of a path.  In the case of our Room Builder, it would be the outline of a couch.  After the path is defined, it is finalized with the click of a button.  At that time, the defined region is turned into an object and added to the children of the canvas along with a click event so that object may be selected again.

You can download the source code for this Room Builder application here.  It won't run for you, though, because there is no server side application with the correct web services.  SORRY.  If anyone really needs that piece, I'll be happy to send it.  It's just time I didn't have tonight to spend.  Sorry.

Anyway,

Here's the key points of this region building operation:

Click the Get Started Button, Cursor changes to a pencil.

public void StartMappingClick(object sender, RoutedEventArgs e)
{
//if it's not visible (Opacity), don't execute it.  Stray Clicks.
if ( StartButton.Opacity < 1)
return;
if (_room == null || string.IsNullOrEmpty(_room.ProductID))
{
message.Text = "Please setup your room first.";
return;
}
_gettingRoom = false;
SBHideStartButton.Begin();
SBShowCancelButton.Begin();
SBShowEndButton.Begin();
//for our cursor
ImageMapperCanvas.Cursor = Cursors.None;
LayoutRoot.Cursor = Cursors.None;
///////////////////////
_points = new List();
// Capture mouse and update stat
CaptureMouse();
if (pencil != null)
{
pencil.Visibility = Visibility.Visible;
if (_positionLast != null)
movePencilToCurrentPosition((Point)_positionLast);
}
lastRectangle = "";
_bIn = true;
this.message.Text = "";
}

Click at each turning point of your region

private void HandleMouseLeftButtonDown(object sender, MouseEventArgs e)
{
// Capture mouse and update stat
Point pos = e.GetPosition(this);
if (_bIn)
{
//highlight the point and add it to the list 
registerPoint( pos );
}
_positionLast = pos; //this is so we can track for edits.
}
private void registerPoint(Point pos)
{
//only register the point if it's inside the image.
if (!CheckCollision(pencil, ImageMapperCanvas))
return;
_points.Add(pos);
if (_points.Count == 1)
{
//mark the point
Ellipse el = new Ellipse();
el.SetValue(Canvas.LeftProperty, pos.X+20);
el.SetValue(Canvas.TopProperty, pos.Y+20 );
el.Width=2;
el.Height=2;
el.Stroke = new SolidColorBrush(Color);
el.StrokeThickness = 1;
el.SetValue(Line.NameProperty, lineName(_points.Count));
LayoutRoot.Children.Add(el);
}
else
{
//draw the polyline and add the line to the list.
//string of points
//
// Create new line
var line1 = new Line
{
X1 = _positionLast.Value.X,
Y1 = _positionLast.Value.Y,
X2 = pos.X,
Y2 = pos.Y,
Stroke = new SolidColorBrush(Color),
StrokeThickness = 1
};
line1.SetValue(Line.NameProperty, lineName(_points.Count));
LayoutRoot.Children.Add(line1);
}
}

Click the End Mapping Button

public void EndMappingClick(object sender, RoutedEventArgs e)
{
//if it's not visible (Opacity), don't execute it.  Stray Clicks.
if (this.EndButton.Opacity < 1)
return;
SBHideStartButton.Begin();
SBHideEndButton.Begin();            
SBShowcontrolgrid.Begin();
// Release mouse and update stat
ReleaseMouseCapture();
ImageMapperCanvas.Cursor = Cursors.Stylus;
LayoutRoot.Cursor = Cursors.Arrow;
if (pencil != null)
{
pencil.Visibility = Visibility.Collapsed;
}
if (_bIn)
{
_ictr++;
//we could switch this out based on a selected tool.
// MapItemBO newMapItem = drawRectangle();
MapItemBO newMapItem = drawPolygon();
if (newMapItem != null)
persistMapItem( newMapItem );
_bIn = false;
_points.Clear();
}
this.message.Text = "Search for the outlined item";
}

During this end mapping process, a path object is created, a click event attached, and that object is added to the children of the canvas.

code to get this path added to the canvas, by the way, if you download the sample code, this is line 509 in page.xaml file.

private void createPolygonPath(string dataline, string shapeName, Color border)
{
//to get this I used code from CreateWPFShape.  I also brought up Blend to
//help me with the Xaml for the path, expecially fill and opacity.
//also, in my diagram maker is code which created path geometries programatically
//this came from scribbler.
//
//extract the point array from this dataline. remove markup from the dataline.
dataline = dataline.ToLower();
int ilen = dataline.IndexOf("coords=");
if (ilen < 7)
return; 
dataline = dataline.Substring(ilen + 7);
ilen = dataline.IndexOf("href");
if (ilen < 1)
return;
dataline = dataline.Substring(0,ilen);
dataline = dataline.Replace("\"", "");
//message.Text = dataline;  //for debugging.
string[] pointlist = dataline.Split(','); //split the dataline on commas into an array.
if (pointlist.Count() < 2)
return;
// Create a new geometry.
PathGeometry geometry = new PathGeometry();
// Create a new path figure.
PathFigure figure = new PathFigure();
figure.Segments = new PathSegmentCollection();
// Add figures to the geometry.
for (int i = 0; i < pointlist.Count(); i++)
{
// Determine the starting index and the end index
// into the points array that defines the figure.
int ptx = -1;
int.TryParse( pointlist[i], out ptx);
int pty = -1;
if ( pointlist.Count() > 1 && i != (pointlist.Count() - 1) )
int.TryParse( pointlist[i + 1], out pty);
else
int.TryParse( pointlist[ pointlist.Count()-1 ], out pty);
System.Windows.Point pt = new Point( ptx, pty);
if (i == 0)
figure.StartPoint = pt;
else
{
LineSegment linesegment1 = new LineSegment();
linesegment1.Point = pt;
figure.Segments.Add(linesegment1);
}
i++; //move to the next x
}
if (geometry.Figures == null)
{
PathFigureCollection pathFigures = new PathFigureCollection();
pathFigures.Add(figure);
geometry.Figures = pathFigures;
}
else
{
// Add the new figure to the geometry.
geometry.Figures.Add(figure);
}
// Add the geometry to a new Path and set path properties.
System.Windows.Shapes.Path path = new System.Windows.Shapes.Path();
path.Data = geometry;
path.SetValue(Canvas.NameProperty, shapeName);
SolidColorBrush brw = new SolidColorBrush(Colors.White);           
path.Fill = brw;
path.Opacity = .40;
SolidColorBrush brb = new SolidColorBrush( border ); 
path.Stroke = brb;
path.MouseLeftButtonUp += new System.Windows.Input.MouseButtonEventHandler(editItemClick);
LayoutRoot.Children.Add(path);
}

Happy SILVERLIGHT! 

GOTCHA - or Learning how to be productive in Silverlight application development.

by Bobbi Perreault 11. October 2008 00:07
Share on Facebook

This blog post is part of my series of posts taken from a presentation I gave in September on SEO for Silverlight Applications.

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
Part III, service layer communicatioons - the Glue between the Silverlight and the WebApp
Part IV, Maintaining Browser History in a Silverlight Application

Now the adventures in Silverlight Begin.

SO – commonly encountered problems. Uh, I'm still really nuts about Silverlight and I get so excited to be able to work in it. So please take that statement and keep it in mind while I rant.

I was recently able to participate in the development of a rather ambitious Silverlight application. One which taught me a lot about how to optimize my time, how to find what's really broken, and how to keep the dang debugger attached.

It seems that the .xap file is cached by the browser. It doesn’t seem like it – it is. The Xap file is Cached by the browser.

So this is Good if you’re a web client and you want good response time. This is REALLY HARD if you’re a Silverlight developer trying to make and test a series of incrementally small changes to your Silverlight application. This is doubly complicated if you've got a Silverlight application that uses a separate .dll for business logic. Changing the Business Logic .dll will not "register" with the environment to send a new xap. Either that, or Internet Explorer doesn't think it should have to download a new .xap file to the client for whatever reason.

You know you've got an old copy of the .xap in your cache if your debugging breakpoints are disabled when the control is loaded in the browser. So although this is easily identified and it is easily fixed by clearing your temporary files, It took me literally DAYS to figure it out. Heck, I tried everything. Close Visual Studio, ReBoot My Computer, Search the Internet for answers. That’s Heartbreaking when you're on a fixed timeline.

So, anyway, here’s the magic combination: Tools/Internet Options/Delete/Delete Files/YES/Close/OK
Remember that, Tools/Internet Options/Delete/Delete Files/YES/Close/OK

That's my new routine, I think I should actually add it as a post build action for my silverlight project. Whenever it gets compiled, it will clear the temporary files before launching the debugger when I F5.

Xaml binding to a non-existant object is another problem you may encounter as I did while working in Silverlight.

Two different bindings that have caused me trouble here. One binding is when you add code to your XAML element to bind it’s data to an object that lives in the code behind. The opening edit screen wil be displayed and you’ll be able to enter text in an input area. But as soon as your focus moves from the first , the screen will go white. If that happens, the object in the code behind is null. Just make sure you always have an object created and that you haven't bound to a null object. (makes sense. :-) )

A second white screen situation results when using these techniques I’ve shown you today, for example where the button.xaml template is inserted into the xaml. If those binding styles aren’t present in your resources somewhere you won’t be able to see what happened and the app won’t work.

Normally when I run into this, I like to solve this problem by loading my control up in BLEND, as Blend provides better feedback in some error conditions. But when the xaml has been added dynamically you need to build a throwaway version of your control to get it into blend – or just be very meticulous in comparing your application resources to the resources your code is inserting.

Data-caching – edit a record and save the changes. Bring up the same record again, and the original data, sent down in the first request is used again to load this Silverlight control. Nnavigate to another page, come back in, and you still get the old record. Browsers are notorious at hanging on tenaciously to cached pages. This problem is evident in Ajax applications when sending XMLHTTPRequest GET requests to Internet Explorer. Even when you use all manner of fancy headers like "Pragma: no-cache" or "Cache-Control: must-revalidate" you'll often find that you receive a cached page rather than a 'live' one. Here's something else you can try, which has worked well for me, and essentially only needs one line of Javascript.

		myRand=parseInt(Math.random()*99999999);  // cache buster

As Always, I thank all the generous programmers and contributors who have put these answers up to their Internet sites - you people save my job every day and I would be working in a different line of business if you didn't make your contributions. thanks!

Maintaining Browser History in a Silverlight Application

by Bobbi Perreault 10. October 2008 23:37
Share on Facebook

The purpose of maintaining browser history is to give you the ability to enable the browser back button and code for deep linking which improves your discoverability.

This blog post is part four 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
Part III, service layer communicatioons - the Glue between the Silverlight and the WebApp

Maintaining history and deep linking ability

There are other ways to do this, maintaining browser history for back button and deep linking. In these early days of MVC, though, it’s a little bit to me like the Wild Wild West. What I mean is sometimes it just feels like every man for himself and the man with the biggest gun is the law. So, I brought out the big gun for my browser history and found myself a jquery plugin that is supposed to do the job.

Using this jQuery plugin, jquery.history - I’ve linked to the file menu.js which we’ve looked at before. In the function that sets up all the actions I need to complete when the page loads, ( that’s the $(document).ready function), I've placed a function call that initializes the history object.

 
function pageload(hash) {
// hash doesn't contain the first # character.
if(hash) {
// restore ajax loaded state
$("#load").load(hash + ".html");
} else {
// start page
$("#load").empty();
}
}

Every time a Silverlight object invokes a script that loads a new Island, it also invokes this Function silverlightHistory with the parameter (detailsurl) . This is the script which sets my new url up in the address bar, and adds me to history.

   
// set onlick event for buttons
function silverlightHistory( detailsurl ){
// 
var hash = detailsurl;
hash = hash.replace(/^.*#/, '');
// moves to a new page. 
// pageload is called at once. 
$.historyLoad(hash);
return false;
}
function setProductDetails( detailsurl )
{
//setup for browser history
silverlightHistory( detailsurl );
$('#sl-productListDisplay').html(" ").fadeOut();
//fetch the silverlight product details and plug it in, it's controlid #2
$.get('/Content/sl.htm', function(data){ 
setSilverlight(data, 'ControlId', 2, 'sl-productListDisplay', detailsurl);})
}

service layer communications - the Glue between the Silverlight and the WebApp

by Bobbi Perreault 5. October 2008 12:54
Share on Facebook

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:

  1. Silverlight doesn't natively support serialization
  2. Silverlight objects in binary form must be in a separate runtime. (double the fun)
  3. 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,

  1. The constructor hooks up the page loaded event and sets a member var to the value received for requested into the _url member.
  2. 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.

 

RSS Feed FriendFeed