Silverlight TagCloud Now on CodePlex

September 28, 2009 06:27

The complete Silverlight TagCloud series of posts:

  1. A Silverlight TagCloud, Part 1: The WCF Service
  2. A Silverlight TagCloud, Part 2: The TagCloud
  3. A Silverlight TagCloud, Part 2.1: Refinements
  4. Silverlight TagCloud Now on CodePlex

The Silverlight TagCloud control, like my Live Comment Preview control, now has its very own project page on CodePlex:

image

 

I'll continue to post information about upgrades and new features (if there are any) on this blog, but the source will henceforth live on CodePlex. Feel free to leave feedback either here or on the new Silverlight TagCloud project site.

Download: Silverlight TagCloud control on CodePlex


Silverlight Live Comment Preview Control: Some Touch-up and Now on CodePlex

September 23, 2009 19:48

The complete Silverlight Live Comment Preview series of posts:

  1. Silverlight Live Comment Preview Control
  2. Silverlight Live Comment Preview Control: Some Touch-up and Now on CodePlex

I've learned a thing or two since the initial post about my Silverlight Live Comment Preview control, including how to use all of the WPF colors in Silverlight, how to set initial parameters and use the <object> tag, and how to allow for better customization of all sorts of attributes like fonts and colors. I'd already leveraged much of this newfound knowledge by fixing up my Silverlight Tag Cloud control, so I thought it a good time to revisit my Live Comment control and touch it up a bit as well.

Some of the biggest problems with that first implementation was that if you wanted to change things like font family, font color, font size, the background color, or even the initial text to display upon initialization (you might need to display some initial text on a postback if processing a failed captcha, for example), you had to make those changes in the Silverlight control itself, recompile, and deploy the new control. What a pain… Not to mention the control did not auto-size with respect to its container width.

The New and Improved Silverlight Live Comment Preview Control

I implemented all of those features and even wrapped the control in an ASP.NET User Control to boot. Now, to use the control, you start with a Register declaration:

   1: <%@ Register src="~/UserControls/LiveCommentPreview.ascx" TagName="LiveCommentPreview" TagPrefix="sl" %>

Then declare the control like this:

   1: <sl:LiveCommentPreview runat="server" BackgroundColor="AliceBlue" FontColor="Orange"
   2:     FontFamily="Tahoma" FontSize="14" InitialPreviewText="Initial [b]formatted[/b] text." />

For BackgroundColor and FontColor, you can use any of the available WPF color names. FontFamily and FontSize are what you might expect. Last, InitialPreviewText is the initial text you want rendered in the control. In general, you can exclude this parameter, but there are cases where I found it useful. The most notable was if someone failed a recaptcha, then on the postback, and since the comment box still contained the user's comment, then the Live Comment Preview control should already contain that same text (but formatted, of course).

Last, the control's width now auto-sizes. See it in action by going to any post on this blog (say, this one, for example), scroll down to the bottom, and adjust the browser width. You can also see it in action by downloading the source code and running the included web site.

Here's the control set into the context of this web site:

image

Silverlight Live Comment Preview Control: Now on CodePlex

image

The Silverlight Live Comment Preview control is now available for download on CodePlex.

Full source, example usage, etc. are available there. Licensing is such that you can do whatever you like with the control, except you'll need to leave the attribution in place (or, rather, I'd appreciate you leaving it in place ;-) ).

Good luck and have fun. Of course, any ideas for new functionality or if you have problems, leave a comment here or on the CodePlex project page.

Download: Silverlight Live Comment Preview Control on CodePlex


A Silverlight TagCloud, Part 2.1: Refinements

August 11, 2009 14:21

The complete Silverlight TagCloud series of posts:

  1. A Silverlight TagCloud, Part 1: The WCF Service
  2. A Silverlight TagCloud, Part 2: The TagCloud
  3. A Silverlight TagCloud, Part 2.1: Refinements
  4. Silverlight TagCloud Now on CodePlex

Maybe this post should be "Part 3", but I think of it as more of an incremental increase over Part 2 of this series, so we'll leave it at 2.1. In any case, this is the third post in a series about a Silverlight TagCloud control I wrote that began with a discussion of the TagCloud's back-end WCF service and then moved on to the front-end Silverlight control. This post extends the Silverlight control, adding some additional features while easing deployment.

Let's get into it.

New Stuff

First, a quick summary of the TagCloud control changes:

1. Removed style information from app.xaml

We don't need it anymore as this information is now passed in as initial parameters.

2. New ItsCodingTime.Utils.Silverlight dll

This utility dll contains the ColorNames class that I previously discussed. I wanted it here to ease distribution and to use in other Silverlight projects. Also, I'll likely write a separate post (and release the full source) about the utils dll when it has some real meat in it.

3. New FontFamily initial parameter

Tag item FontFamily can now be set via a property from ASP.NET.

4. New BackgroundColor initial parameter

The control's Background can now be set via a property from ASP.NET.

5. New TagColor initial parameter

Tag item ForegroundColor can now be set via a property from ASP.NET.

6. New TagHoverColor initial parameter

Tag item MouseOver Foreground color can now be set via a property from ASP.NET.

7. Consolidated the control's ASP.NET and JavaScript code into a user control

The hosting ASP.NET page was getting a little messy what with all of that <object> stuff and JavaScript. While I left the silverlight.js and jquery references, as well as the onSilverlightError function, in my master page, the TagCloud specific code is now in a user control.

FontFamily, BackgroundColor, TagColor, TagHoverColor properties & the SilverlightTagCloud User Control

It's probably best to explain the usage of the new properties and user control with an example. I'll assume you've already installed the TagCloud from my previous post and want to "upgrade".

You'll need to copy over the SilverlightTagCloud.ascx/SilverlightTagCloud.ascx.cs files into your site's "user control" folder (wherever that may be based on your blogging or web site platform) and add the following reference to the page which will house the control:

   1: <%@ Register src="~/User controls/SilverlightTagCloud.ascx" TagName="SilverlightTagCloud"
   2:     TagPrefix="ucSilverlightTagCloud" %>

Once you've got that, you can then replace the entire <object> declaration with (replacing the colors with selections of your own, of course; for possible color names check out my post on WPF Colors in Silverlight):

   1: <ucSilverlightTagCloud:SilverlightTagCloud runat="server" TagThreshold="2" FontFamily="Arial"
   2:     TagColor="#5C80B1" TagHoverColor="DodgerBlue" BackgroundColor="AliceBlue" MinimumFontSize="12" />

Also, there was some initialization code in the code-behind. That can all be removed.

If any of this is hard to follow, check out the sample project in the code download below.

As you can see, the end result of these changes is really focused at being able to customize the appearance of the control via settable properties. Hopefully I've got enough features baked in now, though adding additional properties of your own should be pretty easy.

I won't go over the innards of the user control. It's all the same as what I talked about before (except for the addition of the code to support the above changes), and you can readily check it out in the download. Note that the WCF service code, as before, is in "demo" mode, meaning you can uncomment the BlogEngine.NET specific code if you're using that platform (and include a reference to BlogEngine.NET), otherwise you'll need to plug in your own blogging platform's tag information.

Questions, concerns, or requests for missing or new functionality, let me know.

Download: Silverlight TagCloud control on CodePlex


A Silverlight TagCloud, Part 2: The TagCloud

July 29, 2009 08:36

The complete Silverlight TagCloud series of posts:

  1. A Silverlight TagCloud, Part 1: The WCF Service
  2. A Silverlight TagCloud, Part 2: The TagCloud
  3. A Silverlight TagCloud, Part 2.1: Refinements
  4. Silverlight TagCloud Now on CodePlex

This is the second part of a two part series about a Silverlight TagCloud I developed. In Part 1 of this series I discussed the WCF service component. This part will be about the Silverlight control itself.

A Brief Re-introduction to the TagCloud Architecture

The architecture is a simple producer/consumer model. The producer is a WCF service that gathers tag information. The consumer is the Silverlight control, which gets and renders those tags.

Assuming you're on the itscodingtime.com web site and not an RSS reader, you can see the TagCloud in action at the right.

Some of the Difficulties

Developing this control would have been a piece of cake if not for one thing: resizing. Think about it. The control needs to respond to resize events based on (1) browser resizing and (2) the number of tags being displayed. I was able to make use of a WrapPanel control with a TextBlock for each tag, which greatly simplified some of the work. But the WrapPanel will gladly overflow beyond its container if you let it.

Now, you might be able to get away with not worrying about #1 depending on how your web site is laid out, but #2 is a hard one to get around. That's not to say my solution is the only one or the best. There may well be a 100% width here and a 100% height there method that works (maybe even across all browsers), but I couldn't figure it out.

I did, however, come up with a control that quite nicely resizes based on browser sizing and tag information, so let's get into the good stuff.

The Silverlight TagCloud Control

First of all, as always, the full source is available for download below. In the .zip, you'll find projects for the TagCloudService (see Part 1) and TagCloudControl, as well as a basic web app that hosts the service and control and should prove useful in terms of testing. One thing of note: I commented out the BlogEngine.NET specific code in the WCF service and put in a dummy loop to generate tag data. This way you can run the project without having BlogEngine.NET in place.

The TagCloud control uses two controls from the Silverlight Toolkit: WrapPanel and TextBlock. You'll probably want to download the control toolkit if you plan on doing any debugging (though I do include System.Windows.Controls.dll in the .zip download).

Here's the project layout so you can get a feel for what files are involved:

image

Pretty standard stuff, though note that I've already added the service reference to TagCloudService and a strong name key file for the Silverlight control.

Let's get into the control's files.

1.) App.xaml

I defined one style in the Application's XAML file (per the Future Enhancements list below, I plan to add the ability to specify your own font family as well as tag color, so this style will be going away at some point).

   1: <Application xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
   2:          xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" 
   3:          x:Class="TagCloud.TagCloudControl.App">
   4:     <Application.Resources>
   5:         <Style x:Key="TagStyle" TargetType="HyperlinkButton">
   6:             <Setter Property="Foreground" Value="#d0eb55" />
   7:             <Setter Property="FontFamily" Value="Tahoma" />
   8:         </Style>
   9:     </Application.Resources>
  10: </Application>

2.) App.xaml.cs

If you look in App.xaml.cs, you'll see that the control receives a small number of Initial Parameters: baseUrl, controlId, controlHostId, tagThreshold, minimumFontSize. Here's the Application_Startup method:

   1: private void Application_Startup (object sender, StartupEventArgs e)
   2: {
   3:     if (e.InitParams.Count <= 0)    // no initial param's, no continue
   4:     {
   5:         return;
   6:     }
   7:  
   8:     // NOTE: We'll look for missing params and set defaults in the Page constructor
   9:  
  10:     var baseUrl = string.Empty;
  11:     var controlId = string.Empty;
  12:     var controlHostId = string.Empty;
  13:     var tagThreshold = string.Empty;
  14:     var minimumFontSize = string.Empty;
  15:  
  16:     if (e.InitParams.ContainsKey ("BaseUrl"))
  17:     {
  18:         baseUrl = e.InitParams["BaseUrl"];
  19:     }
  20:  
  21:     if (e.InitParams.ContainsKey ("ControlId"))
  22:     {
  23:         controlId = e.InitParams["ControlId"];
  24:     }
  25:  
  26:     if (e.InitParams.ContainsKey ("ControlHostId"))
  27:     {
  28:         controlHostId = e.InitParams["ControlHostId"];
  29:     }
  30:  
  31:     if (e.InitParams.ContainsKey ("TagThreshold"))
  32:     {
  33:         tagThreshold = e.InitParams["TagThreshold"];
  34:     }
  35:  
  36:     if (e.InitParams.ContainsKey ("MinimumFontSize"))
  37:     {
  38:         minimumFontSize = e.InitParams["MinimumFontSize"];
  39:     }
  40:  
  41:     RootVisual = new Page (baseUrl, controlId, controlHostId, tagThreshold, minimumFontSize);
  42:  
  43:     return;
  44: }

And a brief description of each initial parameter:

  • baseUrl: This is the base web site url passed in via the hosting site. I have this because I've deployed the control to multiple sites, so this is an easy way to have the site identify itself. Note that you could also use Application.Current.Host.Source, then chop off the "ClientBin/Control.xap" part. I might even go that route as a future enhancement and simplification.
  • controlId: This is the Silverlight control's id from ASP.NET. We need this for sizing.
  • controlHostId: This is the hosting div's id, also from ASP.NET. We need this for sizing.
  • tagThreshold: In a TagCloud you typically only want to display those tags whose frequency has surpassed a certain threshold. That's the purpose of this parameter. Set it to 0 if you want to display all tags.
  • minimumFontSize: The minimum font size to use for the "smallest" tag. Tags are sized up proportionally from there.

All of those parameters are then handed off to the control's Page class.

3.) Page.xaml

Here's the Page's XAML:

   1: <UserControl xmlns:controlsToolkit="clr-namespace:System.Windows.Controls;assembly=System.Windows.Controls.Toolkit" x:Class="TagCloud.TagCloudControl.Page"
   2:     xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" 
   3:     xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
   4:     Loaded="UserControl_Loaded"
   5:     xmlns:d="http://schemas.microsoft.com/expression/blend/2008" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
   6:     mc:Ignorable="d">
   7:     <controlsToolkit:WrapPanel x:Name="uxWrapPanel" SizeChanged="WrapPanel_SizeChanged" />
   8: </UserControl>

Note that I am referencing the Silverlight Control Toolkit here since we are using a WrapPanel.

4.) Page.xaml.cs

This is where all of the work is done. I won't go through every part, but instead try to hit the highlights.

a.) ScriptableMember

In the Page class constructor we register a scriptable object in order to build a JavaScript to Silverlight bridge:

   1: HtmlPage.RegisterScriptableObject ("JStoSLBridge", this);

The method UpdateControlSize is then called from ASP.NET/JavaScript and is initiated from, appropriately enough, the browser resize event:

   1: [ScriptableMember]
   2: public void UpdateControlSize (int nNewWidth)
   3: {
   4:     if (HtmlPage.Document != null)
   5:     {
   6:         var silverlightControlHost = HtmlPage.Document.GetElementById (_tagCloudControlHostId);
   7:         var silverlightObjectTag = HtmlPage.Document.GetElementById (_tagCloudControlClientId);
   8:  
   9:         if ((silverlightControlHost != null) && (silverlightObjectTag != null))
  10:         {
  11:             silverlightObjectTag.SetStyleAttribute ("width", nNewWidth + "px");
  12:             silverlightControlHost.SetStyleAttribute ("width", nNewWidth + "px");
  13:         }
  14:     }
  15:  
  16:     return;
  17: }

b.) The WCF TagCloudService Proxy

Here's where the baseUrl parameter comes in:

   1: var binding = new BasicHttpBinding ();
   2: var address = new EndpointAddress (new Uri (Application.Current.Host.Source, baseUrl + "TagCloudService.svc"));
   3: _tagCloudService = new TagCloudServiceClient (binding, address);

Turns out I'm using Application.Current.Host.Source anyway. A good cleanup step here might be to just parse out the base url from Application.Current.Host.Source and not use the baseUrl parameter at all. In any case, this initializes our connection to the TagCloud WCF service from which we will be retrieving tag data. However you construct or get the web site's address, it's best to dynamically create the WCF endpoint since it alleviates some of the labor when deploying to multiple sites.

c.) Retrieving Tag data

Once the control has been loaded we fire off an asynchronous call to the TagCloudService:

   1: private void UserControl_Loaded (object sender, RoutedEventArgs e)
   2: {
   3:     if (_tagCloudService != null)
   4:     {
   5:         _tagCloudService.GetTagsCompleted += tagCloudService_GetTagsCompleted;
   6:         _tagCloudService.GetTagsAsync (TagThreshold, BaseUrl);
   7:     }
   8:  
   9:     return;
  10: }

Of note is the TagThreshold parameter, which the service will use to only return those tags which meet the specified minimum threshold (no point in sending back all tag data if the control isn't going to make use of it).

I'll spare you the whole tagCloudService_GetTagsCompleted method; here's the main part:

   1: foreach (var cloudTag in _cloudTagCollection)
   2: {
   3:     if (cloudTag.TagOccurrences < TagThreshold)    // if tag occurrence is below threshold, do not add; the service shouldn't return these, but just in case
   4:     {
   5:         continue;
   6:     }
   7:  
   8:     if (cloudTag.Equals (null)) continue;
   9:  
  10:     // we'll wrap a TextBlock in the HyperlinkButton in order to support text wrapping
  11:     var textBlock = new TextBlock { Text = cloudTag.TagName, TextWrapping = TextWrapping.Wrap };
  12:  
  13:     var lnkBtn = new HyperlinkButton
  14:     {
  15:         Content = textBlock,
  16:         FontSize = CalcTagFontSize (cloudTag.TagOccurrences, maxOccurrences),
  17:         NavigateUri = new Uri (cloudTag.TagLink),
  18:         HorizontalAlignment = HorizontalAlignment.Stretch,
  19:         VerticalAlignment = VerticalAlignment.Stretch,
  20:         HorizontalContentAlignment = HorizontalAlignment.Center,
  21:         VerticalContentAlignment = VerticalAlignment.Center,
  22:         Style = Application.Current.Resources["TagStyle"] as Style
  23:     };
  24:  
  25:     lnkBtn.MouseEnter += TagCloudItem_MouseEnter;    // we'll do some styling on mouse over
  26:     lnkBtn.MouseLeave += TagCloudItem_MouseLeave;    // we'll restore styling on mouse leave
  27:  
  28:     uxWrapPanel.Children.Add (lnkBtn);
  29: }

We loop through the received tags, creating a new TextBlock for each and then wrapping that TextBlock in a HyperlinkButton so that when a user clicks on the tag it takes them to that series of pages.

What it's iterating through is a collection of CloudTag objects, defined as part of the TagCloudService's data contract. More on that can be found in Part 1 of this series.

d.) Calculating Tag Font Size

One of the core features of a TagCloud is, of course, the ability to display tags in varied sizes based on their frequency. Thanks goes to poeticcode for supplying the tag sizing algorithm:

   1: private double CalcTagFontSize (int tagOccurrences, int maxTagOccurrences)
   2: {
   3:     if (maxTagOccurrences == 0)    // this probably shouldn't happen, but you never know...
   4:     {
   5:         return (MinimumFontSize);
   6:     }
   7:  
   8:     // from http://poeticcode.wordpress.com/2007/01/27/tag-cloud-algorithmlogicformula/
   9:  
  10:     var percent = 150 * (1.0 + ((1.5 * tagOccurrences) - (maxTagOccurrences / 2.0)) / maxTagOccurrences);
  11:     var fontSize = (percent / 100) * MinimumFontSize;
  12:  
  13:     return (fontSize);
  14: }

Resizing the TagCloud Control

This is a big enough part of the whole control that I thought it deserved its own section. To be honest, I went back and forth trying to consolidate the resizing code either in the control or purely in JavaScript. No matter how you look at it, you really need resizing in both places. That's because we have to potentially resize the control based on browser resizing as well as the number of tags and their size. While it's possible to put the control in such a position in the browser that it's never affected by browser sizing, but you still have to deal with resizing based on tag content. The WrapPanel will resize itself, but not the actual control. Throw in browser incompatibilities and this issue wound up not being trivial at all.

Let's take a look.

1.) Control Sizing within the Control

There are two places within the control where sizing occurs. First, we have the before-mentioned UpdateControlSize:

   1: [ScriptableMember]
   2: public void UpdateControlSize (int nNewWidth)
   3: {
   4:     if (HtmlPage.Document != null)
   5:     {
   6:         var silverlightControlHost = HtmlPage.Document.GetElementById (_tagCloudControlHostId);
   7:         var silverlightObjectTag = HtmlPage.Document.GetElementById (_tagCloudControlClientId);
   8:  
   9:         if ((silverlightControlHost != null) && (silverlightObjectTag != null))
  10:         {
  11:             silverlightObjectTag.SetStyleAttribute ("width", nNewWidth + "px");
  12:             silverlightControlHost.SetStyleAttribute ("width", nNewWidth + "px");
  13:         }
  14:     }
  15:  
  16:     return;
  17: }

UpdateSize is a ScriptableMember, which means it can be called from JavaScript. Here's that scripting code from the ASP.NET page:

   1: function UpdateTagCloudSize() {
   2:  
   3:     var silverlightControl = document.getElementById('silverlightTagCloud');
   4:     if (silverlightControl == null) return;
   5:  
   6:     var sidebarWidth = jQuery("#sidebar").css("width");
   7:     var newCtrlWidth = parseInt(sidebarWidth); // removes the "px" at the end
   8:     newCtrlWidth -= 10; // some tweaking just for this site
   9:  
  10:     silverlightControl.content.JStoSLBridge.UpdateControlSize(newCtrlWidth); // reminder: don't need "px" at the end
  11: }
  12:  
  13: // addEvent function by John Resig: http://ejohn.org/projects/flexible-javascript-events/
  14: function addEvent(obj, type, fn) {
  15:     if (obj.attachEvent) {
  16:         obj['e' + type + fn] = fn;
  17:         obj[type + fn] = function() { obj['e' + type + fn](window.event); }
  18:         obj.attachEvent('on' + type, obj[type + fn]);
  19:     }
  20:     else
  21:         obj.addEventListener(type, fn, false);
  22: }
  23:  
  24: // add Resize event handler
  25: addEvent(window, 'resize', function(event) {
  26:     UpdateTagCloudSize();
  27: });

Courtesy of John Resig we have an event handler that hooks into the browser's resize event. As the browser is resized, we make a call to our UpdateControlSize via our JavaScript-to-Silverlight Bridge mechanism. Resizing challenge #1 handled.

The other place we deal with resizing is via the WrapPanel's SizeChanged event handler inside the Silverlight control:

   1: private void WrapPanel_SizeChanged (object sender, SizeChangedEventArgs e)
   2: {
   3:     if (HtmlPage.Document != null && _tagsLoaded)    // don't start resizing until content loaded
   4:     {
   5:         var silverlightControlHost = HtmlPage.Document.GetElementById (_tagCloudControlHostId);
   6:         var silverlightObjectTag = HtmlPage.Document.GetElementById (_tagCloudControlClientId);
   7:  
   8:         if ((silverlightControlHost != null) && (silverlightObjectTag != null))
   9:         {
  10:             silverlightObjectTag.SetStyleAttribute ("height", e.NewSize.Height + "px");
  11:             silverlightControlHost.SetStyleAttribute ("height", e.NewSize.Height + "px");
  12:         }
  13:     }
  14:  
  15:     return;
  16: }

We passed in the control id's from ASP.NET to the control, so that's how we know the clientId and hostId. We then modify their heights and, if not for an initial sizing issue, we'd be done. Unfortunately, I had to throw in an additional kick-start to get the control properly sized when it first loads.

2.) Kick-starting the Control Sizing with some more JavaScript/jQuery

Not that big of a deal, but I had to give the control a kick in the butt when the page is first loaded. We accomplish this initial sizing with some help from jQuery (if you're new to jQuery, don't sweat it—I included the .js file in the project and that's all you really need for this):

   1: jQuery(document).ready(function() {
   2:  
   3:     var silverlightControlHost = document.getElementById('silverlightControlHost');
   4:     var sidebarWidth = jQuery("#sidebar").css("width");
   5:     sidebarWidth = parseInt(sidebarWidth); // removes the "px" at the end
   6:  
   7:     sidebarWidth -= 10; // bit of a tweak based on the ict template, this will keep the control from overflowing the right border
   8:  
   9:     // resize the control host, NOT the control itself

  10:     silverlightControlHost.style.width = sidebarWidth + "px";
  11: });

Using jQuery's "ready" function, which is essentially an event handler for when the page is ready for user interaction, we get some width information that is very specific to this blog template. That means you'll have to tweak this code to do what you need based on your own web site layout.

Conclusion

That's the Silverlight TagCloud control. I've already come up with a short list of future enhancements below, so look for an update sometime in the future.

Until next time.

Download: Silverlight TagCloud control on CodePlex

Future Enhancements

Because there's always room for improvement.

  1. Pass in tag font family as parameter
  2. Pass in tag font color as parameter
  3. Pass in control background color as parameter
  4. Clean-up some of the sizing (if possible)

References

TagCloud Algorithm/Logic/Formula

Tag Clouds Gallery: Examples And Good Practices