Pages

Sunday, August 16, 2009

AutoResizing ComboBox Silverlight

AutoResizing ComboBox Silverlight

All Silverlight 2.0 enthusiasts have faced an annoying known issue with a databound ComboBox:

the dropdown popup resizes itself only the first time it is shown.

After its first initialization, no matter if you bind a new datasource with fewer or more elements, the dropdown persists its original height.

One workaround is the following:

  1. store the Properties from the original ComboBox
  2. delete the ComboBox removing it from its container
  3. create a new ComboBox and place it in the container
  4. recover the stores Properties
  5. bind the new DataSource to the newly created combobox

Well, many of us will agree this workaround is annoying as the original issue was...

Here is my try to create a new ComboBox inheriting the original one.

It overrides the OnItemsChanged method, trying to guess the height needed to render the Items within the dropdown popup.


using System.Windows.Controls;

namespace CodeGolem.Controls
{
public class ComboBox : System.Windows.Controls.ComboBox
{
private double _maxDropDownHeight = 0;

public new double MaxDropDownHeight
{
get { return _maxDropDownHeight; }
set { _maxDropDownHeight = value; }
}

protected override void OnItemsChanged(System.Collections.Specialized.NotifyCollectionChangedEventArgs e)
{
base.OnItemsChanged(e);

ScrollViewer scrollViewer = (ScrollViewer)GetTemplateChild("ScrollViewer");
scrollViewer.ScrollToVerticalOffset(0);
scrollViewer.ScrollToHorizontalOffset(0);


Border popupBorder = (Border)GetTemplateChild("PopupBorder");
double height = (ActualHeight - 1) * Items.Count + 5;
popupBorder.Height = _maxDropDownHeight > 0 && height > _maxDropDownHeight ? _maxDropDownHeight : height;
}
}
}

I tried this only without templated Items.
The final height is calculated supposing that the items' height is equal to the ComboBox height minus one.
I add also five pixels for upper and lower margins.

The height-calculating formula may be improved or changed to reflect your own needs.

Anyway I think having a custom ComboBox control within our projects is less annoying than re-creating each combo everytime we update their datasource.

Hope this helps all you readers... while we all confidently wait for the final Silverlight 3.0 release!

Themes are a great feature in our beloved ASP.NET platform.

Themes are a great feature in our beloved ASP.NET platform.

They allow us to easily create skins for our controls and we can bind stylesheets to each theme.

But did you notice most of the times we use a main color in a theme's stylesheet, and two or three other related colors, and we continously repeat their #rrggbb code along the whole .css files?
Most of the times, different themes simply means different color.
And there we go customizing the stylesheet files to repleace that red with this blue, etc.

Here I'm going to explain a way to create a Theme-based styleheet parameterizer which will allow us to place variable placeholders in our .css files.
The variables' values can be assigned from a Theme-dependant .xml file, our we could customize this parameterizer to get the values from a database.

The StylesheetParameterizer will be implemented as an HttpHandler which will handle each request to .css files, parse them looking for the variable placeholders, and replace their value depending on the current theme.

Since an HttpHandler is not theme-aware, we will need an HttpModule as well. It will listen to each page request and store the page's Theme name in a Session variable that will later be accessed by the HttpHandler.

We will start from the HttpModule:


using System;
using System.Web;
using System.Web.UI;

public class StylesheetParameterizerModule : IHttpModule
{
#region IHttpModule Members

public void Dispose()
{
}

public void Init(HttpApplication context)
{
context.PostRequestHandlerExecute += new EventHandler(context_PostRequestHandlerExecute);
}

void context_PostRequestHandlerExecute(object sender, EventArgs e)
{
if (HttpContext.Current.Handler is Page)
HttpContext.Current.Session["StylesheetParameterizerTheme"] = ((Page)HttpContext.Current.Handler).Theme;
}

#endregion
}

This is a simple IHttpModule implementation, handling the PostRequestHandlerExecute event.
After a request has been executed, this module verifies if the executed handler was a Page.
If so, it stores the Page's Theme property in a Session variable.

This module has to be registered in the web.config file in the section ( in IIS7).



Now let's give a look at the HttpHandler.


using System.IO;
using System.Text.RegularExpressions;
using System.Web;
using System.Web.SessionState;

public class StylesheetParameterizerHandler : IHttpHandler, IReadOnlySessionState
{
DataTable _parameters;

#region IHttpHandler Members

public bool IsReusable
{
get { return false; }
}

public void ProcessRequest(HttpContext context)
{
string requestedStylesheetPath = context.Request.PhysicalPath;
string stylesheet = string.Empty;

using (StreamReader reader = new StreamReader(requestedStylesheetPath))
{
stylesheet = reader.ReadToEnd();
reader.Close();
}

string cacheKey = "ParameterizedStylesheet_" + context.Session["StylesheetParameterizerTheme"].ToString();
if (context.Cache[cacheKey] != null)
stylesheet = context.Cache[cacheKey].ToString();
else
{
_parameters = getParameters(context.Session["StylesheetParameterizerTheme"].ToString());

Regex regex = new Regex("\\$.+\\$", RegexOptions.IgnoreCase);
stylesheet = regex.Replace(stylesheet, matchEvaluator);

context.Cache.Add(
cacheKey,
stylesheet,
null,
System.Web.Caching.Cache.NoAbsoluteExpiration,
TimeSpan.FromMinutes(context.Session.Timeout),
System.Web.Caching.CacheItemPriority.Normal,
null);
}

context.Response.Write(stylesheet);
}

#endregion
}

One important thing to note: the class implements the IReadOnlySessionState marker-interface.
An HttpHandler is not allowed to access the SessionState by default.
Implementing this interface tells the framework that the HttpHandler needs to access the SessionState, in read only mode.

The ProcessRequest method is where the magic occurs.

The handler reads the physical file that was requested (the .css stylesheet file), and stores its full content in a local string variable.
The handler looks then for the variable plaholders through the \$.+\$ regular expressions.
This means our placeholders will have this format within a stylesheet:

body
{
color: $color$;
}

In this example $color$ will be a placeholder that will be substituted by the "color" variable value defined in the parameter xml file.

Each match will be handled by the matchEvaluator() method.

This is the matchEvaluator() body


private string matchEvaluator(Match match)
{
string name = match.Value.Substring(1, match.Value.Length - 2);
DataRow row = _parameters.Rows.Find(new object[] { name });

return row["value"].ToString();
}

Parameters are stored in a DataTable that will be populated by the getParameters() method.
The matchEvaluator looks for a row in the table whose "name" column value equals the name of the matched variable in the stylesheets.
It then returns the corresponding value as a substitution.

Finally, this is the getParameters() implementation.


private DataTable getParameters(string theme)
{
DataTable parameters;

string parametersXmlPath = string.Format("~/App_Themes/{0}/StylesheetParameters.xml", theme);

DataSet dataSet = new DataSet();
dataSet.ReadXml(HttpContext.Current.Server.MapPath(parametersXmlPath));
parameters = dataSet.Tables[0];
parameters.PrimaryKey = new DataColumn[] { parameters.Columns["name"] };

return parameters;
}

This method reads the StylesheetParameters.xml file within the current Theme's folder.
The xml file is parsed as a DataTable and will have the following format:


This implementation can be customized for retrieving the parameters from different other stores: database, remote webservices, web.config configuration file, etc.

A little note about Caching: the whole modified stylesheet is stored in the Application Cache so it can be shared by any Session in the same application.
The stylesheet is stored in Cache with a sliding expiration equal to the timeout period of a Session.
This will prevent too much overload since this handler will be invoked on each page request.

To get it working, we have to register the handler in the web.config configuration file


That's all.

Now we can put a StylesheetParameters.xml file in each Theme's folder, and have a single stylesheet with variable placeholders set as needed.

I hope you will find the StylesheetParameterizer helpful in your projects.

Feedback will be greatly appreciated.