Creating an MVC based website with reusable widgets

By Geert van der Cruijsen at July 12, 2009 10:46
Filed Under: Asp.net, Geert's Projects, MVC

After the Asp.net MVC Framework was released i was pretty impressed of how it worked but one of my main problems was how to handle the data on the page that is not the main goal of the specific page.

In ASP.NET MVC every page has its specific controller and views that can handle the data for the page that is requested. for example when you’re building a web shop you’ll probably have a ProductController that handles everything that has to do with the Products in your shop. But when you are browsing a web shop product page you would also like to see other data that has nothing to do with products, for example your shopping basket, current login state etc. Should you add this logic to the ProductController? imo you shouldn’t since the Products controller only responsibility should be the products. So how should we handle this in the MVC framework?

My first idea that came to mind to solve this problem was doing an Ajax request to for example, the shopping basket controller on your view when you would like a shopping basket added to your page. This can be in some situations the best way to do it but in some cases you might not want to use Ajax since then this content can’t be indexed by search engines for example.

An example how to use Ajax widgets is found here: http://www.ajaxprojects.com/ajax/tutorialdetails.php?itemid=310

My main goal was to be able to add widgets to a page without the controller of that page knowing about these widgets. I found some solutions to this called partial requests found here: http://blog.codeville.net/2008/10/14/partial-requests-in-aspnet-mvc/

These partial requests weren’t my favorites also since i didn’t want to put this logic in the view so i developed my own CMS like solution for this problem.

My idea was that a page can have several page zones and page zones contain widgets. widgets and complete page zones should be reusable by other pages so i stored these in my CMS database. how my database is build up you can see in the image below

image

A widget should be able to render itself so i made an abstract BaseWidget class that has a Render Method. A widget can also be a call to a different controller so i made a SubController class that inherits from BaseWidget. The SubController Widget has an Controller, Action and ID so it can call the controller it belongs to.

   1: public abstract class SubControllerWidget : BaseWidget
   2: {
   3:     public string Controller { get;  set; }
   4:     public string Action { get;  set; }
   5:     public object ID { get; set; }
   6:  
   7:     public SubControllerWidget(string Controller, string Action) : base()
   8:     {
   9:         this.Controller = Controller;
  10:         this.Action = Action;
  11:         this.ID = null;
  12:     }
  13:  
  14:     public SubControllerWidget(string Controller, string Action, object ID)
  15:         : base()
  16:     {
  17:         this.Controller = Controller;
  18:         this.Action = Action;
  19:         this.ID = ID;
  20:     }
  21:  
  22:     public override void Render(System.Web.Mvc.ViewContext vc)
  23:     {
  24:         vc.RouteData.Values["controller"] = Controller;
  25:         vc.RouteData.Values["action"] = Action;
  26:         vc.RouteData.Values["id"] = ID;
  27:         IHttpHandler handler = new MvcHandler(vc.RequestContext);
  28:         handler.ProcessRequest(System.Web.HttpContext.Current);
  29:     }
  30:  
  31:     public abstract void SetSettings(WidgetSettings settings);
  32: }

To build a specific widget just inherit a widget from SubControllerWidget and you are ready to go. Set the Controller, Action and ID to call and that specific controller will be called.

To add these page zones and widgets to the pages dynamically i’ve created a CMSController that overrides the OnActionExecuted method so it can insert the page zones and widgets to the model that’s been send to the View.

   1: protected override void OnActionExecuted(ActionExecutedContext filterContext)
   2: {
   3:     if (ViewData.Model == null)
   4:     {
   5:         ViewData.Model = new CMSViewModel();
   6:     }
   7:     string controller = filterContext.RequestContext.RouteData.Values["controller"] != null ? filterContext.RequestContext.RouteData.Values["controller"].ToString() : "";
   8:     string controllerAction = filterContext.RequestContext.RouteData.Values["action"] != null ? filterContext.RequestContext.RouteData.Values["action"].ToString() : "";
   9:     string controllerActionid = filterContext.RequestContext.RouteData.Values["id"] != null ? filterContext.RequestContext.RouteData.Values["id"].ToString() : "";
  10:     IList<PageZone> zones = _cmsRep.GetPageZonesForPage(controller, controllerAction, controllerActionid);
  11:  
  12:     CMSViewModel page = (CMSViewModel)ViewData.Model;
  13:     if (page.PageZones == null)
  14:     {
  15:         page.PageZones = zones;
  16:     }
  17:     else
  18:     {
  19:         foreach (PageZone zone in zones)
  20:         {
  21:             page.PageZones.Add(zone);
  22:         }
  23:     }
  24:  
  25:  
  26:     base.OnActionExecuted(filterContext);
  27: }

 

So now all widgets beloning to a page will be retrieved from the CMSRepository and are added to the CMSViewModel so now it’s the View’s turn to render all widgets.

In my master page I’ve added the following line that is responsible for rendering all widgets:

<% Html.RenderPageZones(Model); %>

For that to work I’ve created a few html extension methods to render the page zones.

   1: public static class HtmlCMSExtensions
   2: {
   3:     public static void RenderPageZones(this HtmlHelper html, CMSViewModel page)
   4:     {
   5:         if (page != null && page.PageZones != null)
   6:         {
   7:             foreach (PageZone zone in page.PageZones)
   8:             {
   9:                 html.RenderPartial("PageZone", zone);
  10:             }
  11:         }
  12:     }
  13:  
  14:     public static void RenderWidgetsInZone(this HtmlHelper html, PageZone zone)
  15:     {
  16:         if (zone != null && zone.Widgets != null)
  17:         {
  18:             foreach (IWidget widget in zone.Widgets)
  19:             {
  20:                 widget.Render(html.ViewContext);
  21:             }
  22:         }
  23:     }
  24: }

In my PageZone view i’ve added the other html extension method called RenderWidgetsInZone so all widgets are rendered by itself.

<%@ Control Language="C#" Inherits="System.Web.Mvc.ViewUserControl<VanDerCruijsen.MVC.CMS.Model.PageZone>" %>
<div id="<%= Html.Encode(Model.Name) %>">
name:<%
   1: = Html.Encode(Model.Name) 
%>
<%
   1:  Html.RenderWidgetsInZone(Model); 
%>
</div>

This is the way I use to create widgets on my ASP.NET MVC pages. If you have any recommendations or better ways how to do it please let me know.

When I have the time I’m going to build an admin controller to be able to add widgets and pagezones via the webinterface since now the only option is to add them in the database by hand.

My sources aren’t really cleaned up so if you want a working source/solution please send me mail or leave a comment.

Geert van der Cruijsen

Last.Fm Radio Stream Player Gadget 0.5 Released!

By Geert van der Cruijsen at July 25, 2008 22:36
Filed Under: Cool Stuff, Geert's Projects

Update 08-06-2010: the last.fm gadget does not work anymore. Last.fm changed their client server protocols and therefore the gadget is not working anymore. I'm not using Last.fm anymore so i'm not planning on updating the gadget in the future

 

 

Last week my mailbox was flooded by mails of people who were telling me my Windows Vista Sidebar gadget to play Last.fm radio was broken.

I tested it myself and concluded that this was indeed the case. Last.fm changed their website last week and my guess is that they also changed their streaming service.

After a full night of debugging I found out what was the problem and I fixed it in a new version which can be now downloaded from the download area.

If you are interested in what was changed read on. The change was fairly simple, the protocol uses 2 handshakes to connect to the service. The first handshake is to connect to the scrobbling service. The second handshake is used to connect to the streaming service. Before last week requesting the mp3 stream was done by sending the sessionid of the first handshake. Now... it seems you have to send the sessionid you get with the second handshake. It took some time to figure this out but now everything seems to work fine again.

Protocol example:

Handshake 1 request:

http://ws.audioscrobbler.com/radio/handshake.php?version=1.4.2.58376&platform=win32&platformversion=Windows%20Vista&username=geertvdcruijsen&passwordmd5=
4d9309866b98863c3c3336e50392831b&language=en&player=winamp

Handshake 1 response from last.fm:

session=bbe5e763d1e1deb988595cecf1c21e9d stream_url=http://87.117.229.85:80/last.mp3?Session=bbe5e763d1e1deb988595cecf1c21e9d subscriber=0 framehack=0base_url=ws.audioscrobbler.com base_path=/radio info_message= fingerprint_upload_url=http://ws.audioscrobbler.com/fingerprint/upload.php permit_bootstrap=0

Handshake 2 request:

http://post.audioscrobbler.com/?hs=true&p=1.2&c=tst&v=1.3.1.1&u=geertvdcruijsen
&t=1217017120&a=2a060e72c7aae132e8fad06b741be806

Handshake 2 response from last.fm:

OK 7e5b82460d2a45e382b81c437ae6a87a http://post.audioscrobbler.com:80/np_1.2 http://87.117.229.205:80/protocol_1.2

Tune to the right radio channel:

http://ws.audioscrobbler.com/radio/adjust.php?session=bbe5e763d1e1deb988595cecf1c21e9d&url=lastfm://artist/muse

Response to channel change request from last.fm:

response=OK url=lastfm://artist/muse stationname= Muse’s Similar Artists discovery=true

Request metadata of stream:

http://ws.audioscrobbler.com/radio/xspf.php?sk=bbe5e763d1e1deb988595cecf1c21e9d
&discovery=0&desktop=1.3.1.1&time=1217017120321

I was happy to receive the mails of complaints because this is proof people are actually using my gadget :) Hopefully all problems are fixed now so happy listening!

Geert van der Cruijsen

Display Picture Metadata in your Silverlight 2.0 Deepzoom Application

By Geert van der Cruijsen at April 30, 2008 17:11
Filed Under: Cool Stuff, Geert's Projects, Silverlight

As i promised 2 days ago here's my post about how to display metadata of sub images in your Silverlight Deepzoom Application. I already typed this post yesterday but because of a stupid mistake i lost my post and now I have to type it again :( (but I think I'll make it a bit shorter)

To start building your Silverlight 2.0 deepzoom application download Deepzoom Composer from Microsoft here. This tool works quite easy. just add some images in the "import" mode then drag them on your screen in the "compose" mode and then export your deepzoom Project.

If you want to be able to identify the different sub images in your Multiscaleimage object check the "Create Collection" checkbox.

deepzoomComposerCollection

After exporting this will be the result:

deepzoomexport

Now to load up this collection of pictures into your own project take a look at this weblog, there is a really good explanation on which steps to take and after you did all those steps i'll explain how to show metada of the different subimages in the deepzoom application.

So if you are at this point you should have your own working deepzoom application and you want to add picture metadata to your project. If not go back to this weblog or try one of these.

The only way of identifying the different sub images in the big multiscaleimage is by the Z-Order of the different images. So how do you know which image has what Z-Order you ask? thats where the generated SparseImageSceneGraph.xml comes in which was generated by the Deepzoom Composer.

<SceneNode

  <FileName> P1000558.JPG</FileName

  <x>0,235460826165879</x>

  <y>0,00108692916410255</y

  <Width>0,218281177677285</Width>

  <Height>0,144923888547009</Height

  <ZOrder>2</ZOrder>

  <Description>Uitzicht van hotelkamer</Description>

</SceneNode>

 

As you can see i added my own Description element to every SceneNode in the generated xml so we can use it in our project. We'll use this xml file to query the ZOrder of the subimage and get the description as a result with the use of Linq.

To do this we'll first have to open the xml file in our code behind page from the page.xaml.cs.

deepZoomObject.Loaded += delegate(object sender, RoutedEventArgs e)

{

    _xmlImageMetadata = XDocument.Load("SparseImageSceneGraph.xml");

};

 

After that we'll add code to display the metadata when the mouse moves over a sub image in the multiscaleimage.

deepZoomObject.MouseMove += delegate(object sender, MouseEventArgs e)

{

    if (mouseButtonPressed) 

    {

        mouseIsDragging = true

    }

    else 

    {

        ImageName.Text = GetMetadata(e.GetPosition(deepZoomObject)); 

    }

    lastMousePos = e.GetPosition(deepZoomObject);

};

 

Then we add the function GetMetadata which queries the xml file and gets the description from it.

private string GetMetadata(Point point)

{

    Point p = deepZoomObject.ElementToLogicalPoint(point); 

    int subImageIndex = SubImageHitTest(p);

    if (subImageIndex >= 0) 

    {

        var q = from c in _xmlImageMetadata.Elements("SceneGraph").Elements("SceneNode"

        where ((string)c.Element("ZOrder")) == (subImageIndex + 1).ToString()

        select (string)c.Element("Description"); 

        if (q != null)

       

            return q.Single();

       

        else

            return ""

    }

    else 

        return "";

}

 

This function uses the function SubImageHitTest which I copied from this weblog:

int SubImageHitTest(Point p)

{

    for (int i = 0; i < deepZoomObject.SubImages.Count; i++) 

    {

        Rect subImageRect = GetSubImageRect(i); 

        if (subImageRect.Contains(p))           

            return i; 

    } return -1;

}

 

Rect GetSubImageRect(int indexSubImage)

{

    if (indexSubImage < 0 || indexSubImage >= deepZoomObject.SubImages.Count)        

        return Rect.Empty;

    MultiScaleSubImage subImage = deepZoomObject.SubImages[indexSubImage]; 

    double scaleBy = 1 / subImage.ViewportWidth;

    return new Rect(-subImage.ViewportOrigin.X * scaleBy, -subImage.ViewportOrigin.Y * scaleBy, 1 * scaleBy, (1 / subImage.AspectRatio) * scaleBy);

}

 

if you build and start your application now you should be able to see your added metadata when you mouseover a sub image in the MultiscaleImage.

My working example can be found here: http://www.vdcruijsen.net/projects/SilverlightDeepzoom/test.html

sourcecode can be downloaded here:

(you'll have to add your own deepzoom object since i removed that from the zip file)

 

Happy Coding!

 

Geert van der Cruijsen

kick it on DotNetKicks.com

Geert's Vista Sidebar Gadget: Last.fm stream player version 0.1 released

By Geert van der Cruijsen at January 06, 2008 13:07
Filed Under: Cool Stuff, Geert's Projects

I've been spending all my spare time this weekend to this project but now the player is ready for release 0.1 beta ;)

You can download the gadget here:

Last.fm Stream Player gadget Download

 

Information about how it's made 

I've used wireshark to learn how the official client was using the last.fm protocol to get a mp3 stream since this wasnt documentated on the last.fm development documentation.

The different states in the protocol are:

  • handshake 1 (for streaming)
  • handshake 2 (for scrobbling)
  • tuning into a channel
  • get metadata of current song
  • send "now playing" information
  • at the end of song submit the scrobbled song
  • go to step 1

Since vista sidebar gadgets are html/javascript only the gadget is using an windows media player activex object to play the actual stream. All the handshake and other http requests are done by ajax calls to the audioscrobbler server. You can download the source on the download page also.

Have fun!

Geert van der Cruijsen

Creating a Microsoft Windows Vista Sidebar Gadget: Last.fm stream player - part 1

By Geert van der Cruijsen at January 04, 2008 21:47
Filed Under: Cool Stuff, Geert's Projects

I’ve been using Windows Vista for 1 year now and I never used the sidebar before. I turned it off in the first week and never turned it back on again. Last week I decided to give it a try again and after filling it with some handy gadgets I thought it was time to build my own.

I didn’t know how to build a gadget so I looked it up on MSDN. There are some samples there but basically sidebar gadgets are small “web parts” totally build in HTML and JavaScript.

My first gadget I’m going to build is a Last.FM music player that can play a music stream from the Last.fm site with peoples favorite artists or with music matching a tag. Like you can do on the Last.fm site yourself.I went searching how I could get this stream to run in my gadget and I found out that Last.fm is using a fairly simple http based protocol which is described at http://www.audioscrobbler.net/development/protocol/I first started out with the 1.1 protocol because I didn’t read that there already was a 1.2 version. The 1.2 protocol has 3 states (handshake, now playing and submission) at the moment I’m writing this the only thing I have made is the handshake part and other functions to get metadata from the track.

I didnt make a nice user interface for my gadget yet and i didnt make anything to save your settings in. This will all be done in the upcoming days. If someone is really good in making a nice little interface (max 60 pixels wide) and feel like helping me out at this give me a call!. I'm not that good at grapic design.

btw. Coding Javascript in Visual Studio 2008 is really really nice. The intellisense really works great and it helps you write code a lot quicker.

I didnt put the sourcecode online yet since it's all in the debugging phase right now and everything is still hard coded (user,password, radio channel etc). When everything is done i'll post the full sourcecode here.

The functions that I’ve build now are shown in the following picture

The javascript code for these functions is shown below. (I will put it all on my site as a gadget when the whole thing is done).

This is all i could do in my spare time the last 2 days. I hope to get a lot further tomorrow and i'll post the results again.

function Play()

{

duration = 0;

document.WindowsMediaPlayer.Stop();

 

GetMetaData();

currentlyPlaying = true;

 

setTimeout("UpdateTimer()", 1000);

 

}

function stop()

{

currentlyPlaying = false;

document.WindowsMediaPlayer.Stop();

}

function UpdateTimer()

{

if (currentlyPlaying)

{

if((document.WindowsMediaPlayer.CurrentPosition < duration) || (duration == 0))

{

document.getElementById("counter").innerHTML = ConvertSecondsToTime(document.WindowsMediaPlayer.CurrentPosition);setTimeout("UpdateTimer()", 950);

 

}

else

{

//alert("restart");

document.WindowsMediaPlayer.Stop();

SendHandshake();

}

}

else

{

stop();

}

}

function ConvertSecondsToTime(seconds)

{

var date = new Date(seconds * 1000);

var timeString = date.getMinutes() + ":" + date.getSeconds();

return timeString;

}

function makeRequest(url) { http_request = false;

if (window.XMLHttpRequest) { // Mozilla, Safari,...

http_request = new XMLHttpRequest();

if (http_request.overrideMimeType) {http_request.overrideMimeType(

'text/xml');

}

} else if (window.ActiveXObject) { // IE

try {

http_request = new ActiveXObject("Msxml2.XMLHTTP");}

catch (e) {

try {http_request =

new ActiveXObject("Microsoft.XMLHTTP");} catch (e) {alert("error");}

}

}

if (!http_request) {

alert('Giving up :( Cannot create an XMLHTTP instance');

return false;

}

http_request.onreadystatechange = alertContents;

http_request.open('GET', url, true); http_request.send(null);

}

function alertContents()

{

if (http_request.readyState == 4)

{

if (http_request.status == 200)

{

 

document.getElementById("debug").innerHTML = http_request.responseText;

 

switch(functionCall)

{

case "SendHandshake" :

GetHandshakeResponse(http_request.responseText);

break;case "TuneIn" :

GetTuneInResponse(http_request.responseText);

break;

case "GetMetaData" :

GetMetaDataResponse(http_request.responseText);

break;default:

alert(functionCall);

}

}

else

{

alert('There was a problem with the request.' + http_request.status);

}

}

}

function SendHandshake()

{

var now = new Date();

now.setHours(now.getUTCHours(),now.getUTCMinutes(),now.getUTCSeconds(),now.getUTCMilliseconds());

var time = parseInt(now.getTime()/1000.0)

 

functionCall = "SendHandshake";

url = "http://post.audioscrobbler.com/?hs=true&p=1.2&c=tst&v=1.3.1.1&u="

url += "geertvdcruijsen";url += "&t=";

url += time;

url += "&a=";

var md5String = MD5("password");

md5String += (time);

url += MD5(md5String);

makeRequest(url);

 

}

function GetHandshakeResponse(response)

{

arr_response = response.split("\n");

//stream_url = arr_response[1].split("stream_url=")[1];

session = arr_response[0].split("session=")[1];document.getElementById("session").innerHTML =response;

TuneIn();

 

}

function TuneIn()

{

functionCall = "TuneIn";url = "http://ws.audioscrobbler.com/radio/adjust.php?session=";

url += session;

url += "&url="

url += "lastfm://artist/Chemical+Brothers/similarartists";url += "&debug=0";

makeRequest(url);

}

function GetTuneInResponse(response)

{

Play();

}

function GetMetaData()

{

var now = new Date();

functionCall = "GetMetaData";url =

"http://ws.audioscrobbler.com/radio/xspf.php?sk=";

url += session;

url += "&discovery=0&desktop=1.3.1.1&time="

url += now.getTime();

makeRequest(url);

 

 

}

function GetMetaDataResponse(response)

{

arr_location = response.split("<location>");stream_url = arr_location[1].split("</location>")[0];

 

arr_duration = response.split("<duration>");duration = arr_duration[1].split(

"</duration>")[0];

 

duration = (parseInt(duration,0)/1000) -3;

document.getElementById("metadata").innerHTML = ConvertSecondsToTime(duration);

 

document.WindowsMediaPlayer.fileName = stream_url;

document.WindowsMediaPlayer.Play();

}

<object id="wmp" standby="standby" type="application/x-oleobject"> <embed type="application/x-mplayer2" id="WindowsMediaPlayer" name="WindowsMediaPlayer" showstatusbar="-1"></embed>

</object>