Wednesday, April 28, 2010

Programmatically Consuming Silverlight Styles

Chris Domino, Director, Enterprise Architect

Hard coding, as anyone who has been development software for more than 20 minutes knows, is bad news. Yet it happens over and over, but probably more as a result of laziness than anything else. And I'm not afraid to say that I've been guilty too. A lot of times, for example, when building a new web site, I am extremely diligent at putting as much styling as possible in my CSS files; my pages' HTML is so pristine that they won't even have style tags!

But then, after staying up all night to meet a dead line, or when a persistent bug regarding an IE 6 compatibility issue refuses to go away, or basically if anything else among the myriad of challenges that building software deals with comes up, it's easy to say "Screw it; I'm hard coding." But for the most part, I think developers are pretty good about using configuration files, style sheets, XSLT; anything that doesn't need to be compiled to keep our software nice and flexible.

These mechanisms are great as long as we can easily pull data out of them. Of course, with components like config files, .NET has the means to consume them, very easily, baked right in. But when it comes to style sheets, things aren't so easy. In fact, I've never needed to programmatically "consume" a style sheet before; it's always simply been welded to my page's HTML.

But with Silverlight, I've started to need this functionality, mainly because we can do so much more with it than HTML. But as is the case whenever code runs on the client, the more we can do means the more we have to do. For example, in the site I'm working on now, I built a pager control that automatically creates buttons for each page, and dynamically resizes itself to fit nicely along with the Canvas it is paging.

One of the problems I had to solve was to show horizontal scrolling for my pager if the number of pager buttons would be beyond the Silverlight control's content region. Here's what it looks like:

If the horizontal scroll bar is visible, I need to make the pager taller. Normally, I would just use ScrollViewer.ComputedHorizontalScrollBarVisibility to do the job. However, with all the padding, dynamic controls, and resizing I do, this property didn't always return value I was expecting. So instead of kludging it, I decided to compute the width myself. This at first sounded daunting, but the logic wasn't too bad. Basically, I sum the widths of all the button controls, and add on the left and right Margin and Padding for each.

The problem is that the margins and paddings were set in styles. (The widths had to be calculated dynamically, since page "10" is wider than page "9" - TextBlock.ActualWidth came to the rescue here.) Now of course this isn't a problem because I could always iterate and accumulate the controls in my pager and get the "actual" width of the control (and therefore determine the need to have a horizontal scrollbar) that way.

However, meh, I thought I could do something a little more elegant. Besides, all it would take is one little UI tweak that could change the behavior of my calculation. So instead, I decided to "pull" these values from the style used on the button one time, store them globally, and then reuse them as needed. This way, the style "owned" these values, and if they ever changed, my logic would continue to work just fine. Yay me.

But then, Silverlight punched me in the face. Basically, the way Style and Setter objects work internally is sort of kludged. I stopped researching when I discovered that my idea wasn't going to work. (Basically Setters only ever set their values on the objects they style exactly one time in the control's lifespan. I believe this is performance enhancement - like how you can't databind to the Position property of a MediaElement even though it's a DependencyProperty.) So to enforce these performance gains, the Style and Setter classes expose very little to us in the API. The Silveright team won't let us hurt ourselves; it's like a child being forced to wear a safety helmet in a dodge ball game.

So did I stop at that? Well, yeah, but it stayed in the back of mind, bothering me all the while I built my pager control. When I finished it early, I decided to use my extra time to go back and try to figure out a way to programmatically access a style in Silverlight.

And I got it! My idea was to use the fact that there is nothing magical AT ALL about styles! You can tell by the XAML and the names of the Setter object's properties ("Property" and "Value") that all they do is reflectively set properties on objects. That's why you can put attached properties and other junk in a style - not just "style" attribute values like in HTML and CSS.

So I wrote a little diddy that, well, basically just uses styles! You give me a style and a property, and I'll instantiate on object of the Style's TargetType type, (until now I've been annoyed that you have to tell a XAML Style what type of object it styles, but this makes my code work!), apply a style, and just reflectively grab that object's property value!

Here's what it looks like:

  1. public static T GetStylePropertyValue<T>(Application app, string styleName, string propertyName)
  2. {
  3. //get style
  4. Style style = app.Resources[styleName] as Style;
  5. if (style == null)
  6. return default(T);
  7. //instantiate an obejct that is the type of this style's target type
  8. FrameworkElement element = (FrameworkElement)style.TargetType.Assembly.CreateInstance(style.TargetType.FullName);
  9. if (element == null)
  10. return default(T);
  11. //apply this style to the new object
  12. element.Style = style;
  13. //use reflection to get the value of the property
  14. PropertyInfo pi = element.GetType().GetProperties().Where(p => p.Name.Equals(propertyName)).FirstOrDefault();
  15. if (pi == null)
  16. return default(T);
  17. //return
  18. return (T)pi.GetValue(element, null);
  19. }

Basically, "T" is the .NET type of the property, "app" is usually App.Current, and along with the name of the style and property, I've got it. So for my pager situation, "T" is "Thickness" and "propertyName" is "Margin." Finally, consuming the margin's dimensions gives me what I need to calculate the total "actual" width of my pager.

Have fun!