Welcome to MSDN Blogs Sign in | Join | Help

Knowledgecast

Interesting solutions to interesting problems
Windows Mobile 6.5 Emulator Available

The developer kit for Windows Mobile 6.5 (including documentation) is now up here: http://www.microsoft.com/downloads/details.aspx?displaylang=en&FamilyID=20686a1d-97a8-4f80-bc6a-ae010e085a6e. You might remember me talking about this at the Tech Ed session last month.

Also included are some samples on the new Gesture APIs. WM 6.5 also supports a new programming model called Widgets (lightweight script applications that run inside an instance of IE6). And yes, WM6.5 does come with Internet Explorer 6. Finally! :)

MOSS: Using the List Web Service to Rename a File

Right, so I ran into an interesting problem recently. I needed to rename a bunch of files in a document library we have on our team’s MOSS site. Problem is, this is a lot of documents. So I figured I’d use MOSS’s web services to do it.

Now in MOSS, a document library is just a specialized list of items (the items just happen to be files). So the web service that I need to use is the MOSS Lists web service (http://msdn.microsoft.com/en-us/library/lists.aspx). Turns out, doing this with the MOSS Lists web service isn’t quite as trivial as I thought it would be (it never is :D).

So here it is, a super simple WPF application that lets you see Document Libraries in a MOSS site and then rename files within a selected document library:

The meat of the logic is in the btnRename_Click handler. I use the user inputs to build up a batch element that tells the UpdateListItems method what to do. More information about the Batch element here: http://msdn.microsoft.com/en-us/library/ms437562.aspx.

The interesting thing about using the Batch element is that not only can you process a whole bunch of files, but you can do different operations on each one of them. What’s more, the OnError flag lets you specify what happens if one of the operations in the Batch runs into an error.

Building Great Looking Mobile Applications

Today at Tech Ed I’ll be trying out something interesting. Instead of speaking on a particular technology area, I’ll be talking about building great looking applications for Windows Mobile. I won’t bother doing a full blown post about this because most of what I will talk about is covered by Alex here: http://msdn.microsoft.com/en-us/library/dd630622.aspx.

In case you were an attendee (I am writing this just before the session) and want access to the Powerpoint deck and demos I’ve got it all packaged up and loaded on my SkyDrive (link below). I realize that some of you will want to re-present this session to an internal audience, so I’ve added demo scripts (word documents) to the demos with talking points and steps of the demo. Do get in touch with me and let me know what you thought of the session.

I’ve also got a mesh folder where I’ll be updating this material. If you want access to that, leave a comment, email me (arunjeet.singh at hotmail.com) or tweet me (@arunjeetsingh).

Right, now I’ve got to go pour some coffee down my throat to put those butterflies in my stomach to rest. Cya at Tech Ed!

Cannon PI

Heh. This could be a lot of fun. I’m looking out for the first episode.

50 points to whoever can tell me who the Some Guy playing Higgins is :)

Tech-Ed Ahoy!

Phew! The demos are done, the Powerpoint deck’s been finalized and I am suffering from a severe lack of sleep. I hope they have lots of coffee at the convention center :)

Tech Ed is an yearly Microsoft event meant for developers, IT Pros and business leaders. This year’s Tech Ed is happening right here, in Hyderabad. For more details: http://www.msteched.in. I am speaking on the Mobility track and will be talking about building good looking UIs for Windows phones. A lot of people get the impression that it isn’t possible to write great looking applications for Windows Mobile. The intention of this session will be to change that opinion and educate people who attend about how easy it is to write good, compelling user interfaces for handheld devices. If you’re coming, do drop me a line in the comments and maybe we could have a sit down.

My First Facebook Application

I did a quick Facebook application to show off at a session I did today. In the process, I ended up writing what might be the beginnings of a Silverlight API for Facebook.

 The application's here. It shows you the status messages of your friends in the order they were last updated. Clicking on a friend's box brings up their photos on Facebook. It's quite a neat little app that took a little under 300 lines of code (XAML + C#) aside from the code that went into the API itself.

I'll post more about this later. For now, enjoy!

Oh, and you'll need to have popups enabled for the application to authenticate you because it's configured as a desktop app at the moment (I know, the irony :D). I'm still trying to figure out how to make it run as a web page without having to write any server-side code :)

A Home for the Collapsible Panel

Phew! It's been a long time since my last post. I have no excuse. I've been a lazy bum and I know it. Well, getting married will do that to ya :)

I have been using the Collapsible Panel control as a tool to demonstrate various Silverlight concepts over the past few months. I started out by building my first content control, then moved on to using VisualTransitions to improve it and finally migrated it to Silverlight 2 RTM when it came out. For me, its been a great way to learn about Silverlight's UI libraries and the intricacies of Silverlight's layout subsystem. This article is another one in this series where I explore writing a container control for the collapsible panel. The code here is by no means final and I am sure there are things that can be improved and features that can be added. Please use the comments link below or the email link at the top to reach out to me with your feedback and suggestions.

Ever since I first put up the collapsible panel control a lot of people have asked me for an elegant way of putting multiple panels on screen. Container controls like StackPanel don't know anything about the CollapsiblePanel expanding or collapsing so they can't animate their content when a panel changes size. What we need is a new container that can respond to the expanding and collapsing of panels that it contains. To get a quick peek at what we're going to build, click here. The demo shows two collapsible panels sitting in a CollapsiblePanelContainer (our new container control) and StackPanel side by side. Notice how the CollapsiblePanelContainer automatically compensates for the panels expanding and collapsing. The source code for the CollapsiblePanel and CollapsiblePanelContainer (along with samples) is here:

Right, lets dive into the code. The first thing that needed to be done was to add an event to the CollapsiblePanel control that the container can hook into. This event simply tells the container (or whoever else is listening) that the CollapsiblePanel is about to change its size. Here's what the event definition looks like:

public event EventHandler<SizeChangingEventArgs> SizeChanging;

 

We trigger the event whenever the IsExpanded dependancy property changes. To do this, we must change the IsExpandedChanged callback method to look like this:

private static void IsExpandedChanged(DependencyObject o, DependencyPropertyChangedEventArgs e)
        {
            bool expanded = (bool)e.NewValue;
            CollapsiblePanel panel = o as CollapsiblePanel;
            if (panel != null)
            {               
                if (expanded)
                {
                    if (panel.SizeChanging != null)
                    {
                        SizeChangingEventArgs se = new SizeChangingEventArgs(new Size(0, panel._expandCollapseButton.ActualHeight), new Size(0, panel._expandCollapseButton.ActualHeight + panel._content.ActualHeight));
                        panel.SizeChanging(panel, se);
                    }

                    _ChangeState(panel, Expand, panel._RollDownStoryboardName);
                }
                else
                {
                    if (panel.SizeChanging != null)
                    {
                        SizeChangingEventArgs se = new SizeChangingEventArgs(new Size(0, panel._expandCollapseButton.ActualHeight), new Size(0, panel._expandCollapseButton.ActualHeight));
                        panel.SizeChanging(panel, se);
                    }

                    _ChangeState(panel, Collapse, panel._RollUpStoryboardName);
                }
            }
        }

 

Note the bold bits. We simply check whether there are any event handlers attached, and depending upon the new value of IsExpanded we either pass the height as just the height of the title (if the panel is collapsing) or the height of the title plus the height of the content (if the panel is expanding). Also notice that I always pass the width as 0. While this isn't technically correct we are only concerned with changing height at the moment, so we'll let that be until the next version :) The SizeChangedEventArgs class, which is derived from RoutedEventArgs carries the old/new size information to the event subscribers.

Finally, we add a new property called VisibleHeight to the CollapsiblePanel:

public double VisibleHeight
        {
            get
            {
                if (IsExpanded)
                {
                    return (_expandCollapseButton.ActualHeight + _content.ActualHeight);
                }
                else
                {
                    return _expandCollapseButton.ActualHeight;
                }
            }
        }

 

The VisibleHeight property is a nice convenient way for the container (or anybody else) to find out what size the CollapsiblePanel is at any given moment.

The real magic happens in a new class called CollapsiblePanelContainer. The CollapsiblePanelContainer class behaves a bit like a StackPanel. CollapsiblePanel instances added to the class are laid out vertically, one after the other. To put some distance between the panels, you can use the Margin property which is respected when CollapsiblePanelContainer does it's layout. The CollapsiblePanelContainer class implements a bunch of attached dependancy properties to keep track of the positions and order of child panels. These properties are:

  • TopProperty (double): The Y co-ordinate position of a panel within the CollapsiblePanelContainer
  • PreviousPanelProperty (CollapsiblePanel): The panel that precedes (is above) a given panel visually
  • NextPanelProperty (CollapsiblePanel): The panel that succeeds (is below) a given panel visually

The implementations of all the attached properties are pretty standard stuff so I won't go into the details of it.

The first thing the CollapsiblePanelContainer has to do is to layout the CollapsiblePanels correctly. This is done in the ArrangeOverride and MeasureOverride methods. These methods correspond to the Silverlight layout system's Arrange and Measure passes (see here for more info on the Silverlight layout system). Here's what our ArrangeOverride method looks like:

protected override Size ArrangeOverride(Size finalSize)
        {
            CollapsiblePanel previousPanel = null;
            double panelTop = 0;

            for (int i = 0; i < Children.Count; i++)
            {
                CollapsiblePanel panel = Children[i] as CollapsiblePanel;
                if (panel != null && (panel as FrameworkElement).Visibility == Visibility.Visible)
                {
                    panel.SizeChanging += new EventHandler<SizeChangingEventArgs>(panel_SizeChanging);

                    if (previousPanel != null)
                    {
                        double previousPanelHeight = 0;

                        if ((previousPanel as FrameworkElement).Visibility == Visibility.Visible)
                        {
                            previousPanelHeight = previousPanel.VisibleHeight;
                            previousPanelHeight += (previousPanel as FrameworkElement).Margin.Top + (previousPanel as FrameworkElement).Margin.Bottom;
                        }

                        panelTop = panelTop + previousPanelHeight;
                        SetTop(panel as UIElement, panelTop);

                        SetNextPanel(previousPanel, panel);
                        SetPreviousPanel(panel, previousPanel);                       

                        _AddStoryboard(panel as UIElement, panelTop);
                    }

                    previousPanel = panel;
                }

                double panelHeight = 0;
                panelHeight = panel.DesiredSize.Height;//.ActualHeight;
                panelHeight += (panel as FrameworkElement).Margin.Top + (panel as FrameworkElement).Margin.Bottom;

                Children[i].Arrange(new Rect(0, panelTop, finalSize.Width, panelHeight));
            }

            return finalSize;
        }

 

The method iterates through each of the panel's children and sets up the Top, PreviousPanel and NextPanel attached properties. Notice that the method only works with CollapsiblePanel objects so at the moment, it isn't possible to put anything other than CollapsiblePanels inside a CollapsiblePanelContainer. The point of the ArrangeOverride method is to workout a rectangular box that can fit the child that needs laying out. Once this rectangle's size and location within the panel has been worked out, the ArrangeOverride method calls the child's Arrange method with the rectangle. This tells the layout system exactly how/where to arrange that child.

In addition to doing the arranging, our implementation of ArrangeOverride also attaches a storyboard to each panel (except the first one which won't need to move in response to other panels moving). This story board allows us to animate a panel if the panels before it expands, contract or move. Notice that we also attach an event handler to each panel's SizeChanging event. As we will see in a little bit, this event handler is where the container triggers the actual animation. Here's what the _AddStoryboard method looks like:

private void _AddStoryboard(UIElement panel, double value)
        {
            TransformGroup tGroup = new TransformGroup();
            TranslateTransform translate = new TranslateTransform();
            translate.Y = 0;
            tGroup.Children.Add(translate);
            panel.RenderTransform = tGroup;

            string sbName = (panel as FrameworkElement).Name + "Animation";

            if (Resources.Contains(sbName))
            {
                Storyboard sb = Resources[sbName] as Storyboard;
                DoubleAnimationUsingKeyFrames anim = sb.Children[0] as DoubleAnimationUsingKeyFrames;
                SplineDoubleKeyFrame keyFrame = anim.KeyFrames[0] as SplineDoubleKeyFrame;
                keyFrame.Value = -(value);
            }
            else
            {
                Storyboard sb = new Storyboard();
                sb.SetValue(NameProperty, sbName);
                DoubleAnimationUsingKeyFrames anim = new DoubleAnimationUsingKeyFrames();
                sb.Children.Add(anim);
                Storyboard.SetTargetName(anim, (panel as FrameworkElement).Name);
                Storyboard.SetTargetProperty(anim, new PropertyPath("(UIElement.RenderTransform).(TransformGroup.Children)[0].(TranslateTransform.Y)"));
                anim.BeginTime = new TimeSpan(0, 0, 0);
                SplineDoubleKeyFrame keyFrame = new SplineDoubleKeyFrame();
                KeySpline spline = new KeySpline();
                spline.ControlPoint1 = new Point(0, 1);
                spline.ControlPoint2 = new Point(1, 1);
                keyFrame.KeySpline = spline;
                keyFrame.KeyTime = new TimeSpan(0, 0, 1);
                keyFrame.Value = -(value);
                anim.KeyFrames.Add(keyFrame);
                Resources.Add(sbName, sb);
            }
        }

 

The method adds a storyboard with the name (panel name) + "Animation". For this reason it is important that CollapsiblePanels put in a CollapsiblePanelContainer have names defined. The Storyboard targets a translate transform which is also added to the panel at this point.

Our implementation of MeasureOverride is pretty straightforward. We simply total up the height of the CollapsiblePanels contained within the CollapsiblePanelContainer and return the total height. Here's what it looks like:

protected sealed override Size MeasureOverride(Size constraint)
        {
            double maxHeight = 0;
            foreach (FrameworkElement child in Children)
            {
                child.Measure(constraint);
                maxHeight += child.DesiredSize.Height;
            }

            return new Size(constraint.Width, maxHeight);
        }

 

Now lets take a look at the panel_SizeChanging event handler:

void panel_SizeChanging(object sender, SizeChangingEventArgs e)
        {
            CollapsiblePanel panel = sender as CollapsiblePanel;
            CollapsiblePanel nextPanel = GetNextPanel(panel);
            if (panel != null && nextPanel != null)
            {
                double goToY = _GetElementTop(panel as UIElement) + e.NewSize.Height + (panel as FrameworkElement).Margin.Bottom + (nextPanel as FrameworkElement).Margin.Top;
                _MovePanel(nextPanel, goToY);
            }
        }

 

Note that because of the SizeChangingEventArgs passed in, we know exactly what the new size of the panel is going to be. All we have to do here is to find the next panel (using the handy NextPanel attached property) and move that panel up (or down) to compensate for the change. To do the moving, we will use the storyboard we added in the ArrangeOverride method. Here's what _MovePanel looks like:

void _MovePanel(CollapsiblePanel panel, double newY)
        {
            CollapsiblePanel nextPanel = GetNextPanel(panel);
            if (nextPanel != null)
            {
                double nextPanelY = newY + panel.VisibleHeight + (panel as FrameworkElement).Margin.Bottom + (nextPanel as FrameworkElement).Margin.Top;
                _MovePanel(nextPanel, nextPanelY);
            }

            if (panel != null)
            {
                string animationResourceName = (panel as FrameworkElement).Name + "Animation";
                Storyboard sb = Resources[animationResourceName] as Storyboard;

                if (sb != null)
                {
                    double goFromY = _GetElementTop(panel as UIElement);
                    double translation = (newY - goFromY) + _GetExistingTranslation(panel as UIElement);
                    ((sb.Children[0] as DoubleAnimationUsingKeyFrames).KeyFrames[0] as SplineDoubleKeyFrame).Value = translation;
                    sb.Begin();
                }
            }           
        }

 

In _MovePanel, we first dig out the CollapsiblePanel that is after the current CollapsiblePanel and recursively call _MovePanel for it. This means that all the CollapsiblePanels below the one that originally changed size will automatically move to compensate for the change. Once we've made the recursive call, we dig out the storyboard for the current CollapsiblePanel and set it's value to the desired translation. Notice that we work out the existing translation first since it is quite possible that the panel we are trying to move has already moved sometime in the past. This would mean that the existing translation would be non-zero (negative if the panel moved up, positive if it moved down) and that we need to take the existing value into account when calculating the new translation.

Finally, to show off what the new container control can do, I added a bunch of CollapsiblePanels to a CollapsiblePanelContainer and sat it side-by-side with a StackPanel also containing CollapsiblePanels. This demonstration can be found in the project called Knowledgecast.DemoApplication project (Page.xaml). To see the code in action, go here and download the code go here:

Well there you have it. Now we've gone and given our CollapsiblePanel a fitting home to live in along with its other brethren. Have fun playing around with the samples and do let me know if you use this code someplace. I love to see my stuff in action :)

Collapsible Panel Makes it to Silverlight 2 RTM

This was something that had been bothering me for a while. Silverlight 2 came out almost two months ago now and I still haven't had the time to go back and update CollapsiblePanel, my sample content control. To read more about CollapsiblePanel, go here and here.

Today I finally updated the code. It took me all of 6 minutes. Here's the changes I made:

1. Moved the generic.xaml from the root of the Knowledgecast.Controls project to a subfolder called Themes. I think this change was introduced for WPF compat.

2. Went into the generic.xaml and ensured that the vsm: prefix was only used for VisualStateManager related tags. In Silverlight 2 Beta 2 Blend would sometimes use this prefix on Style and Setter tags as well. This causes problems with the released version.

That's it! The CollapsiblePanel control is now Silverlight 2 compatible. To see the latest version in action, go here. To get the latest version of the code, go here:

Finally, I should point out that the Silverlight Toolkit (http://www.codeplex.com/Silverlight) has an Expander control that is very similar to the Collapsible Panel. The control is currently in the Preview quality band but the team behind the Toolkit make releases very rapidly so I'm sure it'll move up the quality ladder quickly.

Consuming Feeds with the .NET Syndication API

One of the nicest (and very under-advertised) features to make it into .NET framework 3.5 was the new Syndication API (http://msdn.microsoft.com/en-us/library/system.servicemodel.syndication.aspx). The Syndication API provides .NET applications a great way to consume and publish RSS and Atom feeds. If you've been paying attention to Azure lately you will have realised that Atom forms a major part of Azure and Live services in the cloud. Syndication feeds (Atom/RSS) are fast emerging as the de facto way for applications to update interested parties about changes in state, news and any other updates. There's a good reason for this. Feeds allow consumers (that's you and me) to control when, where and how often to receive updates.

Without further ado, let me present some sample code to consume an RSS feed using the Syndication API:

XmlReader reader = XmlReader.Create("http://blogs.msdn.com/knowledgecast/rss.xml");
Rss20FeedFormatter rss = new Rss20FeedFormatter();
if (rss.CanRead(reader))

  rss.ReadFrom(reader); 
  var items = from item in rss.Feed.Items.OfType<SyndicationItem>() 
                   select new 
                   { 
                      Title = item.Title.Text, 
                      Url = item.Links.First<SyndicationLink>().Uri.AbsoluteUri 
                   };

  lstFeedTitles.DataSource = items; 
  lstFeedTitles.DataBind();
}

reader.Close();

We open an XmlReader with the RSS feed for this blog as URL. We then create an Rss20FeedFormatter (System.ServiceModel.Syndication.Rss20FeedFormatter) and use the CanRead method to verify that the feed loaded in our XmlReader is an RSS feed. The ReadFrom method reads the feed into the feed formatter.

Now things start to get really interesting. I use a clever LINQ query to slurp out the Title and Url of the post into a collection. Note that the objects in this collection all use an anonymouse type that is defined inside the LINQ query. We then bind the resulting collection (which should be an IEnumerable) to a DataList we have sitting in our ASPX page. Here's what our ASPX form looks like:

<form id="form1" runat="server">
    <div>
    <ul>
    <asp:DataList ID="lstFeedTitles" runat="server">
        <ItemTemplate>
         <li>
            <asp:LinkButton ID="lnkTitle" runat="server" Text='<%# DataBinder.Eval(Container.DataItem, "Title")%>' PostBackUrl='<%# DataBinder.Eval(Container.DataItem, "Url")%>' />
         </li>  
        </ItemTemplate>
    </asp:DataList>
    </ul>
    </div>
    </form>

And voila! You have your very own super simple RSS feed reader. It's almost too easy :) Here's what the rendered page looks like:

  • Silverlight 2: So Close I Can Taste It
  • Phew! It's Been a Busy Month
  • Encrypting Configuration Settings in .NET 2.0
  • Using VisualTransition with a Silverlight Content Control
  • Assigning a Name to a Silverlight Element
  • Writing a Silverlight Content Control
  • Microsoft SQL Server 2005 Express Edition with Advanced Services
  • Quick Guide to Silverlight 2 Beta 2
  • Quick Guide to Silverlight 2 Beta 1
  • Are you an aspiring architect?
  • Windows Mobile Power Savers
  • Windows XP Service Pack 3 released
  • Biztalk Server 2006 R3 announced
  • Paste As Text 1.0
  • TechReady 5

    To get this code to work with a proxy server, you can run the XmlReader off a WebClient instead of a direct URL. Here's how you'd do that:

    WebClient wc = new WebClient();
    WebProxy proxy = new WebProxy("myproxy.corp.com", true);
    wc.Proxy = proxy;
    wc.Credentials = CredentialCache.DefaultCredentials;
    Stream s = wc.OpenRead("http://blogs.msdn.com/knowledgecast/rss.xml");

    XmlReader reader = XmlReader.Create(s);

    //Syndication API code here

  • Silverlight 2: So Close I Can Taste It

    No doubt many of you have heard that Silverlight 2 RC0 just debuted. There's loads of bug fixes here since Silverlight 2 Beta 2 and some breaking changes as well. Unlike the breaking changes document from Beta 1 to Beta 2 (which was 80 odd pages) this one's only about 20 pages. To be honest, I don't think it's going to be very much work going from SL2B2 to SL2RC0. Go here for the breaking changes document and here for the release. The tools are meant for Visual Studio 2008 SP1 so make sure you upgrade to that before installing.

    There's also a corresponding version of Expression Blend that came out to support this release. Get Expression Blend 2 Service Pack 1 Preview here.

    Let there be light indeed! :)

    Word of caution though. This version is not intended for releasing applications on, only to help update your applications to work with the eventual Silverlight 2 release.

    Phew! It's Been a Busy Month

    It's been a busy time here in Arunjeetland. Up until a couple of weeks ago, I was working on two projects simultaneously. As I was doing that I had an interesting realization. It's easier to do multiple projects if they're on the same technology. The context switch takes a lot out of you. Unfortunately for me, one of the projects was a Biztalk implementation (with loads of code refactoring) and the other an ASP.NET application. Not easy juggling :)

    I have also been busy prepping for an upcoming Silverlight training that I am supposed to deliver. I was fortunate enough to get my hands on a lot of content that the evangelism folk put out recently, but getting them to fit in the expected formats is a bit of a challenge. Nevertheless, I am learning a lot of new things about Silverlight (finally managed to write a custom panel control) and having a lot of fun with the labs. Oh, and did I mention my sister's getting married and I'm getting engaged next month? Oh yeah, the fun just never ends :D

    Encrypting Configuration Settings in .NET 2.0

    This article describes a utility called ProtectConfig that can be used to Encrypt/Decrypt .NET 2.0 configuration files using the ProtectSection (http://msdn.microsoft.com/en-us/library/system.configuration.sectioninformation.protectsection.aspx) API. The source code of the utility is available here:

    Here's what the ProtectConfig utility looks like:

    image

    If you have ever worked on a large project that involved accessing a database server or other network resources, you will have realised at some stage a lot of potentially sensitive information (credentials, settings etc.) end up inside the configuration files. Ideally, you would want to store all this information in a secure data store. Some information however, such as the database connection strings, are best placed in the configuration file. Thankfully, .NET Framework 2.0 ships with a great utility called aspnet_regiis.exe that can be used to encrypt configuration settings in a web.config file. The encryption can be based on the RSA provider or Windows' own Data Protection API (DPAPI). The RSA provider also allows you to tie the encryption to a user certificate so that you can use the same encrypted data across multiple hosts (provided each of them has the correct certificate in its certificate store). More information about aspnet_regiis.exe can be found here: http://msdn.microsoft.com/en-us/library/ms998280.aspx.

    What a lot of people don't realise is that it is possible to encrypt the settings of any .NET 2.0 (or upwards) application, not just web sites and web applications. What lies behind the aspnet_regiis utility is the ProtectSection method (http://msdn.microsoft.com/en-us/library/system.configuration.sectioninformation.protectsection.aspx) of the SectionInformation class. This method lets you encrypt a configuration section belonging to a .NET configuration file based on a given protection provider. At the moment there are two protection providers (RSA and DPAPI) available out of the box although there is nothing stopping you from writing one.

    Recently, I needed to encrypt a large set of configuration files for a client. We decided to go with the DPAPI provider because we considered it secure enough (it is what Windows machines use to protect logged on user credentials) and we also wanted to tie the encrypted configuration to the machine it was running on. The configuration files to be encrypted were a mixture of background services, web services and web sites. I realised that we would have to provide the deployment team a way to encrypt all these confguration files. My first thought was to go with aspnet_regiis. However, seeing the large number of files and the discomfort the deployment team expressed with a command-line tool I decided to write something that would make their life and mine a bit easier. This was where ProtectConfig was born.

    At the moment, ProtectConfig uses DPAPI to encrypt settings and encrypts the appSettings, connectionStrings, identity and authorization sections. It is smart enough to detect the presence of these sections in the configuration file and then process them. It can also be used to decrypt settings when needed. The really nice thing about ProtectConfig though is that it can process a folder full of .config files (recursively or non-recursively). This makes it easy to encrypt/decrypt all the files on a host in one go.

    Lets take a quick look at the code. The meat of the application lies in two methods: _EncryptConfig and _DecryptConfig. Here's what _EncryptConfig looks like:

    private void _EncryptConfig(string fileName)
            {
                if (String.IsNullOrEmpty(fileName) == false && File.Exists(fileName))
                {
                    lbLog.Items.Add("Analyzing " + fileName);

                    XmlDocument configDom = new XmlDocument();
                    configDom.Load(fileName);
                    bool hasConfig = false;
                    bool hasAppSettings = false;
                    bool hasConnectionStrings = false;
                    bool hasIdentity = false;
                    bool hasAuthorization = false;
                    bool hasSystemWeb = false;

                    XmlNode node = configDom.SelectSingleNode("/*[local-name()='configuration']");
                    if (node != null)
                    {
                        lbLog.Items.Add(fileName + " has a <configuration> section.");
                        hasConfig = true;
                    }
                    else
                    {
                        lbLog.Items.Add(fileName + " has no <configuration> section. This file will not be processed.");
                    }

                    if (hasConfig)
                    {
                        node = configDom.SelectSingleNode("/*[local-name()='configuration']/*[local-name()='appSettings']");
                        if (node != null)
                        {
                            lbLog.Items.Add("Found appSettings");
                            hasAppSettings = true;
                        }

                        node = configDom.SelectSingleNode("/*[local-name()='configuration']/*[local-name()='connectionStrings']");
                        if (node != null)
                        {
                            lbLog.Items.Add("Found connectionStrings");
                            hasConnectionStrings = true;
                        }

                        node = configDom.SelectSingleNode("/*[local-name()='configuration']/*[local-name()='system.web']");
                        if (node != null)
                        {
                            hasSystemWeb = true;

                            XmlNode innerNode = configDom.SelectSingleNode("/*[local-name()='configuration']/*[local-name()='system.web']/*[local-name()='identity']");
                            if (innerNode != null)
                            {
                                lbLog.Items.Add("Found identity");
                                hasIdentity = true;
                            }

                            innerNode = configDom.SelectSingleNode("/*[local-name()='configuration']/*[local-name()='system.web']/*[local-name()='authorization']");
                            if (innerNode != null)
                            {
                                lbLog.Items.Add("Found authorization");
                                hasAuthorization = true;
                            }
                        }

                        ExeConfigurationFileMap map = new ExeConfigurationFileMap();
                        map.ExeConfigFilename = fileName;
                        Configuration config = ConfigurationManager.OpenMappedExeConfiguration(map, ConfigurationUserLevel.None);

                        if (hasAppSettings)
                        {
                            if (config.AppSettings.SectionInformation.IsProtected)
                            {
                                lbLog.Items.Add("appSettings is already protected!");
                            }
                            else
                            {
                                lbLog.Items.Add("Encrypting appSettings");
                                config.AppSettings.SectionInformation.ProtectSection("DataProtectionConfigurationProvider");
                            }
                        }

                        if (hasConnectionStrings)
                        {
                            if (config.ConnectionStrings.SectionInformation.IsProtected)
                            {
                                lbLog.Items.Add("connectionStrings is already protected!");
                            }
                            else
                            {
                                lbLog.Items.Add("Encrypting connectionStrings");
                                config.ConnectionStrings.SectionInformation.ProtectSection("DataProtectionConfigurationProvider");
                            }
                        }

                        if (hasSystemWeb)
                        {
                            ConfigurationSectionGroup systemwebgroup = config.SectionGroups["system.web"];

                            if (hasIdentity)
                            {
                                ConfigurationSection section = systemwebgroup.Sections["identity"];
                                if (section.SectionInformation.IsProtected)
                                {
                                    lbLog.Items.Add("identity is already protected!");
                                }
                                else
                                {
                                    lbLog.Items.Add("Encrypting identity");
                                    section.SectionInformation.ProtectSection("DataProtectionConfigurationProvider");
                                }
                            }

                            if (hasAuthorization)
                            {
                                ConfigurationSection section = systemwebgroup.Sections["authorization"];
                                if (section.SectionInformation.IsProtected)
                                {
                                    lbLog.Items.Add("authorization is already protected!");
                                }
                                else
                                {
                                    lbLog.Items.Add("Encrypting authorization");
                                    section.SectionInformation.ProtectSection("DataProtectionConfigurationProvider");
                                }
                            }
                        }

                        lbLog.Items.Add("Done encrypting information. Saving file " + config.FilePath);

                        config.Save();

                        lbLog.Items.Add("File " + config.FilePath + " saved to disk.");
                    }
                }
                else
                {
                    lbLog.Items.Add("Can't find the file " + fileName);
                }
            }

    The algorithm itself is quite simple. The method first loads up a configuration file in an XmlDocument and checks what sections it has available. After that, armed with this information it goes about encrypting each of these sections using the ProtectSection method of the SectionInformation class. Notice the argument to the ProtectSection method. This argument (DataProtectionConfigurationProvider) is what tells the method to use DPAPI to encrypt the section. To use the RSA provider, simply use the string RsaProtectedConfigurationProvider as argument. To learn how to create and export an RSA key container for use with ProtectSection, see http://msdn.microsoft.com/en-us/library/2w117ede.aspx.

    The code also makes use of the IsProtected property to ensure that it isn't encrypting a configuration section that is already encrypted. The _DecryptConfig method basically reverses the whole process and uses UnprotectSection to decrypt the configuration. Here's what it looks like:

    private void _DecryptConfig(string fileName)
            {
                if (String.IsNullOrEmpty(fileName) == false && File.Exists(fileName))
                {
                    lbLog.Items.Add("Analyzing " + fileName);

                    XmlDocument configDom = new XmlDocument();
                    configDom.Load(fileName);
                    bool hasConfig = false;
                    bool hasAppSettings = false;
                    bool hasConnectionStrings = false;
                    bool hasIdentity = false;
                    bool hasAuthorization = false;
                    bool hasSystemWeb = false;

                    XmlNode node = configDom.SelectSingleNode("/*[local-name()='configuration']");
                    if (node != null)
                    {
                        lbLog.Items.Add(fileName + " has a <configuration> section.");
                        hasConfig = true;
                    }
                    else
                    {
                        lbLog.Items.Add(fileName + " has no <configuration> section. This file will not be processed.");
                    }

                    if (hasConfig)
                    {
                        node = configDom.SelectSingleNode("/*[local-name()='configuration']/*[local-name()='appSettings']");
                        if (node != null)
                        {
                            lbLog.Items.Add("Found appSettings");
                            hasAppSettings = true;
                        }

                        node = configDom.SelectSingleNode("/*[local-name()='configuration']/*[local-name()='connectionStrings']");
                        if (node != null)
                        {
                            lbLog.Items.Add("Found connectionStrings");
                            hasConnectionStrings = true;
                        }

                        node = configDom.SelectSingleNode("/*[local-name()='configuration']/*[local-name()='system.web']");
                        if (node != null)
                        {
                            hasSystemWeb = true;

                            XmlNode innerNode = configDom.SelectSingleNode("/*[local-name()='configuration']/*[local-name()='system.web']/*[local-name()='identity']");
                            if (innerNode != null)
                            {
                                lbLog.Items.Add("Found identity");
                                hasIdentity = true;
                            }

                            innerNode = configDom.SelectSingleNode("/*[local-name()='configuration']/*[local-name()='system.web']/*[local-name()='authorization']");
                            if (innerNode != null)
                            {
                                lbLog.Items.Add("Found authorization");
                                hasAuthorization = true;
                            }
                        }

                        ExeConfigurationFileMap map = new ExeConfigurationFileMap();
                        map.ExeConfigFilename = fileName;
                        Configuration config = ConfigurationManager.OpenMappedExeConfiguration(map, ConfigurationUserLevel.None);

                        if (hasAppSettings)
                        {
                            if (config.AppSettings.SectionInformation.IsProtected)
                            {
                                lbLog.Items.Add("Decrypting appSettings");
                                config.AppSettings.SectionInformation.UnprotectSection();
                            }
                            else
                            {
                                lbLog.Items.Add("appSettings is not encrypted!");
                            }
                        }

                        if (hasConnectionStrings)
                        {
                            if (config.ConnectionStrings.SectionInformation.IsProtected)
                            {
                                lbLog.Items.Add("Decrypting connectionStrings");
                                config.ConnectionStrings.SectionInformation.UnprotectSection();
                            }
                            else
                            {
                                lbLog.Items.Add("connectionStrings is not encrypted!");
                            }
                        }

                        if (hasSystemWeb)
                        {
                            ConfigurationSectionGroup systemwebgroup = config.SectionGroups["system.web"];

                            if (hasIdentity)
                            {
                                ConfigurationSection section = systemwebgroup.Sections["identity"];
                                if (section.SectionInformation.IsProtected)
                                {
                                    lbLog.Items.Add("Decrypting identity");
                                    section.SectionInformation.UnprotectSection();
                                }
                                else
                                {
                                    lbLog.Items.Add("identity is not encrypted!");
                                }
                            }

                            if (hasAuthorization)
                            {
                                ConfigurationSection section = systemwebgroup.Sections["authorization"];
                                if (section.SectionInformation.IsProtected)
                                {
                                    lbLog.Items.Add("Decrypting authorization");
                                    section.SectionInformation.UnprotectSection();
                                }
                                else
                                {
                                    lbLog.Items.Add("authorization is not encrypted!");
                                }
                            }
                        }

                        lbLog.Items.Add("Done decrypting information. Saving file " + config.FilePath);

                        config.Save();

                        lbLog.Items.Add("File " + config.FilePath + " saved to disk.");
                    }
                }
                else
                {
                    lbLog.Items.Add("Can't find the file " + fileName);
                }
            }

    The really great thing about encrypting your configuration using ProtectSection is that you don't need to change the code of your application at all. The code for ProtectConfig is available here:

    Using VisualTransition with a Silverlight Content Control
    Note: The zip archive below has been updated after Silverlight 2 released to the web. For more details on the changes I made, see this post.

    This post is part of series that demonstrates how to write a Silverlight 2 content control. The first post in the series is here. This post mostly improved on the code and concepts covered in the previous post. The latest code for the content control we’re building can be found here:

    Those of you who tried out the CollapsiblePanel control might have noticed something. When the panel first renders on screen, it actually takes two clicks to collapse it back in. I noticed this issue rather late and after some investigation realized what was going on. The IsExpanded flag, which is a boolean, is set to false by default. Clicking the panel the first time it renders sets it to true. However, since the panel renders expanded there is no visual change to be observed. This is why it looks like the panel silently ignores the first click.

    I set out to fix the issue by ensuring that the initial value of IsExpanded is respected. Obviously, if a value isn’t provided (as is the case with the sample that accompanies the panel) the panel must start out collapsed. To begin with, I changed the _content_SizeChanged method from this:


    void _content_SizeChanged(object sender, SizeChangedEventArgs e)
            {
                FrameworkElement content = sender as FrameworkElement;
                if (content != null)
                {
                    TransformGroup tGroup = new TransformGroup();
                    TranslateTransform translate = new TranslateTransform();
                    translate.SetValue(FrameworkElement.NameProperty, "RollTransform" + Guid.NewGuid().ToString());
                    translate.Y = 0;

                    tGroup.Children.Add(translate);
                    content.RenderTransform = tGroup;

                    _RollUpStoryboardName = "RollUp" + Guid.NewGuid().ToString();
                    _RollDownStoryboardName = "RollDown" + Guid.NewGuid().ToString();

                    _SetupYTranslationStoryboard(translate, _RollUpStoryboardName, -content.ActualHeight);
                    _SetupYTranslationStoryboard(translate, _RollDownStoryboardName, 0);
                }
            }

    To this:

    void _content_SizeChanged(object sender, SizeChangedEventArgs e)
            {
                FrameworkElement content = sender as FrameworkElement;
                if (content != null)
                {
                    TransformGroup tGroup = new TransformGroup();
                    TranslateTransform translate = new TranslateTransform();
                    translate.SetValue(FrameworkElement.NameProperty, "RollTransform" + Guid.NewGuid().ToString());

                    if (IsExpanded)
                    {
                        translate.Y = 0;
                    }
                    else
                    {
                        translate.Y = -content.ActualHeight;
                    }

                    tGroup.Children.Add(translate);
                    content.RenderTransform = tGroup;

                    _RollUpStoryboardName = "RollUp" + Guid.NewGuid().ToString();
                    _RollDownStoryboardName = "RollDown" + Guid.NewGuid().ToString();

                    _SetupYTranslationStoryboard(translate, _RollUpStoryboardName, -content.ActualHeight);
                    _SetupYTranslationStoryboard(translate, _RollDownStoryboardName, 0);
                }
            }

    The _content_SizeChanged event is called whenever the size of the content changes. Setting up the translation to –content.ActualHeight when IsExpanded is false means that the content is hidden when the panel is first shown. This is exactly what we expected. When this code is run, the panel we see on screen looks like this:

    Collapsed panel

    Notice something interesting? We took care of the content, but the title animation, which is defined by a visual state still needs handling. Easy enough, I hear you say :). We’ll just use the Visual State Manager to set the state to Collapsed. Okay then, let’s see what the code looks like when we do that:

    if (IsExpanded)
                    {
                        translate.Y = 0;
                        VisualStateManager.GoToState(this as Control, Expand, true);
                    }
                    else
                    {
                        translate.Y = -content.ActualHeight;
                        VisualStateManager.GoToState(this as Control, Collapse, true);
                    }

     


    For brevity’s sake, I dropped the code surrounding the if construct this time. This looks like it should do what we want it to and indeed it does. However, those of you with a sharp eye (or fast computers) may have noticed something. As soon as the panel first appears on screen, you see the little arrow in the title bar going from the expanded to the collapsed state. Which is to say that while the GoToState does set the visual state to Collapsed, it does so using the animation defined by that state. From Silverlight’s point of view of course this is the right thing to do. However, it doesn’t feel quite right from our perspective. So how do we fix this?

    To fix the problem we must first understand how GoToState works. When GoToState is called with a control and a visual state, it simply digs out the corresponding state’s storyboard and begin’s playing it. It does not care whether or not the story board contains an animation. You might have noticed the third parameter to GoToState that I have not discussed so far. This third parameter tells GoToState whether it should get to the state it is going to using transitions along the way. You would think that setting the parameter to false like this:

    VisualStateManager.GoToState(this as Control, Collapse, false);

    would finally fix the issue for us. But that won't be the case, because as I said earlier, the GoToState method does not know whether or not the Expand state's storyboard is an animation. Fortunately, the people who wrote Visual State Manager thought of this scenario and this is where VisualTransition comes in. The documentation has this to say about VisualTransition:

    (VisualTransition) Represents the visual behaviour that occurs when the control transitions from one state to another

    That is to say that while the storyboards contained within visual states define how we want our control to look when it is in a particular state, visual transitions define how our control gets from one state to another. A visual transition, like visual states is defined in the control's template. Let's take a look at what the CollapsiblePanel's template would look like with a visual transition that takes it from the Expanded to the Collapsed state (and vice versa):

    <vsm:Style TargetType="local:CollapsiblePanel">
            <vsm:Setter Property="Template">
                <vsm:Setter.Value>
                    <ControlTemplate TargetType="local:CollapsiblePanel">
                        <Grid>                    
                            <vsm:VisualStateManager.VisualStateGroups>                           
                                <vsm:VisualStateGroup x:Name="CommonStates">
                                    <vsm:VisualState x:Name="Collapse">
                                        <Storyboard>
                                            <DoubleAnimationUsingKeyFrames Storyboard.TargetName="ArrowAngleTransform" Storyboard.TargetProperty="Angle" BeginTime="00:00:00">
                                                <SplineDoubleKeyFrame KeyTime="00:00:00" Value="0">
                                                    <SplineDoubleKeyFrame.KeySpline>
                                                        <KeySpline ControlPoint1="0,1" ControlPoint2="1,1"/>
                                                    </SplineDoubleKeyFrame.KeySpline>
                                                </SplineDoubleKeyFrame>
                                            </DoubleAnimationUsingKeyFrames>
                                        </Storyboard>
                                    </vsm:VisualState>                               
                                    <vsm:VisualState x:Name="Expand">
                                        <Storyboard>
                                            <DoubleAnimationUsingKeyFrames Storyboard.TargetName="ArrowAngleTransform" Storyboard.TargetProperty="Angle" BeginTime="00:00:00">
                                                <SplineDoubleKeyFrame KeyTime="00:00:00" Value="90">
                                                    <SplineDoubleKeyFrame.KeySpline>
                                                        <KeySpline ControlPoint1="0,1" ControlPoint2="1,1"/>
                                                    </SplineDoubleKeyFrame.KeySpline>
                                                </SplineDoubleKeyFrame>
                                            </DoubleAnimationUsingKeyFrames>
                                        </Storyboard>
                                    </vsm:VisualState>
                                   
                                   
                                    <vsm:VisualStateGroup.Transitions>
                                        <vsm:VisualTransition x:Name="Collapse2Expand" From="Collapse" To="Expand" Duration="00:00:01">
                                            <Storyboard>
                                                <DoubleAnimationUsingKeyFrames Storyboard.TargetName="ArrowAngleTransform" Storyboard.TargetProperty="Angle" BeginTime="00:00:00">
                                                    <SplineDoubleKeyFrame KeyTime="00:00:01" Value="90">
                                                        <SplineDoubleKeyFrame.KeySpline>
                                                            <KeySpline ControlPoint1="0,1" ControlPoint2="1,1"/>
                                                        </SplineDoubleKeyFrame.KeySpline>
                                                    </SplineDoubleKeyFrame>
                                                </DoubleAnimationUsingKeyFrames>
                                            </Storyboard>
                                        </vsm:VisualTransition>
                                        <vsm:VisualTransition x:Name="Expand2Collapse" From="Expand" To="Collapse" Duration="00:00:01">
                                            <Storyboard>
                                                <DoubleAnimationUsingKeyFrames Storyboard.TargetName="ArrowAngleTransform" Storyboard.TargetProperty="Angle" BeginTime="00:00:00">
                                                    <SplineDoubleKeyFrame KeyTime="00:00:01" Value="0">
                                                        <SplineDoubleKeyFrame.KeySpline>
                                                            <KeySpline ControlPoint1="0,1" ControlPoint2="1,1"/>
                                                        </SplineDoubleKeyFrame.KeySpline>
                                                    </SplineDoubleKeyFrame>
                                                </DoubleAnimationUsingKeyFrames>
                                            </Storyboard>
                                        </vsm:VisualTransition>
                                    </vsm:VisualStateGroup.Transitions>

                                </vsm:VisualStateGroup>
                            </vsm:VisualStateManager.VisualStateGroups>
                            <Grid.RowDefinitions>
                                <RowDefinition Height="20" />
                                <RowDefinition />
                            </Grid.RowDefinitions>
                            <Grid x:Name="ExpandCollapseButton" Height="20" Grid.Row="0">
                                <Path x:Name="TitleBack" Opacity="0.8" HorizontalAlignment="Stretch" Margin="0,0,0,0" VerticalAlignment="Stretch" Stretch="Fill" StrokeThickness="0.5" Data="M12.5,7 C47.333332,7 115.85664,7 117,7 C118.14336,7 122.1255,6.7291665 122.25,12 C122.3745,17.270834 122.25,18.333334 122.25,21.5 L12.5,21.5 z">
                                    <Path.Fill>
                                        <RadialGradientBrush GradientOrigin="0.699000000953674,0.792999982833862">
                                            <RadialGradientBrush.RelativeTransform>
                                                <TransformGroup>
                                                    <ScaleTransform CenterX="0.5" CenterY="0.5" ScaleX="1.4" ScaleY="2.188"/>
                                                    <SkewTransform CenterX="0.5" CenterY="0.5"/>
                                                    <RotateTransform CenterX="0.5" CenterY="0.5"/>
                                                    <TranslateTransform X="0.017" Y="0.009"/>
                                                </TransformGroup>
                                            </RadialGradientBrush.RelativeTransform>
                                            <GradientStop Color="#FF00008B" Offset="1"/>
                                            <GradientStop Color="#FFADD8E6" Offset="0"/>
                                        </RadialGradientBrush>
                                    </Path.Fill>
                                </Path>
                                <TextBlock Cursor="Arrow" HorizontalAlignment="Stretch" Margin="27.75,2.75,-5,1.75" VerticalAlignment="Stretch" FontFamily="Verdana" FontSize="11" FontStyle='Normal' FontWeight='Normal' Foreground='#FFFFFFFF' Text='{TemplateBinding Title}' Opacity='1' x:Name='Title'/>
                          <Path x:Name="Arrow" HorizontalAlignment="Left" Margin="5.85300016403198,2.81200003623962,0,3.29800009727478" VerticalAlignment="Stretch" Width="10.685" Fill="#FFFFFFFF" Stretch="Fill" Stroke="#FF000000" StrokeThickness="0" Data="M182.75038,211.50015 L216.5,234.50017 L182.81238,257.87216 z" RenderTransformOrigin="0.5,0.5">
                           <Path.RenderTransform>
                            <TransformGroup>
                             <RotateTransform x:Name="ArrowAngleTransform" Angle="90"/>
                            </TransformGroup>
                           </Path.RenderTransform>
                          </Path>  
                      </Grid>
                        <Grid x:Name="ContentContainer" Grid.Row="1">
                            <Border x:Name="PanelContent" BorderThickness="0">
                                <Grid>
                                        <Rectangle Opacity="0.6" Stroke="Gray" HorizontalAlignment="Stretch" VerticalAlignment="Stretch"/>
                                        <Rectangle Opacity="0.6" Stroke="Black" HorizontalAlignment="Stretch" VerticalAlignment="Stretch">
                                            <Rectangle.RenderTransform>
                                                <TranslateTransform X="-1" Y="-1" />
                                            </Rectangle.RenderTransform>
                                        </Rectangle>
                                        <ContentPresenter />
                                </Grid>
                            </Border>
                        </Grid>
                        </Grid>
                    </ControlTemplate>
                </vsm:Setter.Value>
            </vsm:Setter>
        </vsm:Style>

    I highlighted the new code in red. Also notice that I changed the duration on the storyboards for the states. Both the Collapse and Expand storyboard now run for 0 seconds, which is to say that they are instantaneous. As you can see, the transitions live in a collection hanging off of the Visual State Group object. Each transition expects to be provided the name of a start state, and end state and the duration for which the transition must run. The storyboard for the transition looks exactly like the state storyboard (it doesn't have to though) except that it lasts for a second. Now that we have separated the states from their transitions this code:

    VisualStateManager.GoToState(this as Control, Collapse, false);

    should do what we want. Setting the last parameter to false means that while it will run the state storyboard, it will ignore the transition storyboard. When the parameter is true the transition storyboad is run first and upon its completion the state storyboard is begun. Let's take a look at what the _content_SizeChanged method finally ends up looking like:

    void _content_SizeChanged(object sender, SizeChangedEventArgs e)
            {
                FrameworkElement content = sender as FrameworkElement;
                if (content != null)
                {
                    TransformGroup tGroup = new TransformGroup();
                    TranslateTransform translate = new TranslateTransform();
                    translate.SetValue(FrameworkElement.NameProperty, "RollTransform" + Guid.NewGuid().ToString());
                    if (IsExpanded)
                    {
                        translate.Y = 0;
                        VisualStateManager.GoToState(this as Control, Expand, false);
                    }
                    else
                    {
                        translate.Y = -content.ActualHeight;
                        VisualStateManager.GoToState(this as Control, Collapse, false);
                    }

                    tGroup.Children.Add(translate);
                    content.RenderTransform = tGroup;

                    _RollUpStoryboardName = "RollUp" + Guid.NewGuid().ToString();
                    _RollDownStoryboardName = "RollDown" + Guid.NewGuid().ToString();

                    _SetupYTranslationStoryboard(translate, _RollUpStoryboardName, -content.ActualHeight);
                    _SetupYTranslationStoryboard(translate, _RollDownStoryboardName, 0);
                }
            }

    There you go! Now our brand new content control supports visual states as well as visual transitions. That's all the features of the Visual State Manager covered! We have ensured that CollapsiblePanel respects the initial value of IsExpanded and also fixed that annoying little bug :)

    For my next post, I will take a look at how to change our panel so that we can retemplate it to collapse horizontally. If you've been reading this series and enjoying the posts on this blog, please do leave behind a comment and rate this post. The latest code for CollapsiblePanel (along with the changes for this post) is available here:

    The previous post in this series is available here.

    Assigning a Name to a Silverlight Element

    I answered this in a comment earlier today but I figured this was something that deserved a post of its own. There seems to be some confusion around how to set the name (x:Name in XAML) of a Silverlight object in code. Here's how it's done:

    object.SetValue(FrameworkElement.NameProperty, "objectName");

    This has the same affect as setting x:Name in XAML. The name property is a dependancy property implemented by the FrameworkElement abstract base class. This class is inherited by most Silverlight elements.

    Writing a Silverlight Content Control
    Note: The zip archive below has been updated after Silverlight 2 released to the web. For more details on the changes I made, see this post.

    Note: I updated the article and attached code after samcov pointed out some issues with the earlier code. 

    This article illustrates the writing of a Silverlight content control. Click here to see it in action. It is part of multi-post series. You can find the next post, which is about Visual Transitions here. If you’d rather just dive into the code and explore it yourself then it is available for download here:

    Here's what we are going to build:

    Preview

    All the content in the panel (including the text, the textbox and the radio button) is defined by the consumer of the control (the page where the panel is embedded). This is the sort of thing that content controls enable in Silverlight. Now lets get on with figuring out how it's done.

    For my first Silverlight project recently, I needed to do a panel based interface. I decided to do collapsible panels so that I could present as much information as possible while still giving users a choice to only see what they wanted to. Originally, I started out with a whole bunch of user controls but as you can imagine, updating and maintaining each of those user controls got very tedious very quickly.

    The only thing different between these user controls was the actual content in them. This got me thinking. Is there a better way to abstract out the container while still keeping the content different? I found my answer in content controls. The Silverlight button, for instance, is a content control. The wonderful thing about a content control is that you can leave the decision of what is going to go into the control up to the consumer of the control and just concentrate on how the overall control behaves. The content can be anything ranging from simple text to elaborate graphics.

    I found some great blog posts and walkthroughs on writing basic content controls for Silverlight 2 Beta 1, but nothing that used the new Visual State Manager that shipped with Silverlight 2 Beta 2. I decided there might be other people interested in doing what I have done so I wrote this post.

    The first thing to do when writing a content control is to inherit from the System.Windows.Controls.ContentControl. This class is in the System.Windows.dll assembly. Start with a Silverlight class library project and add your content control class:

    public class CollapsiblePanel : ContentControl
    {
    public CollapsiblePanel()
            {
                DefaultStyleKey = typeof(CollapsiblePanel);
            }
    }

    We will call our content control CollapsiblePanel because it is a panel that the user can expand or contract at runtime. Notice the DefaultStyleKey property that I set in the constructor. This property identifies the default style for the control and is usually set to the type of the control. This is useful if for instance you want to inherit a control (such as Button) but do not want to create a whole new default style template for it. In such a case, you would just set the DefaultStyleKey to point to the type of the base class.

    The next thing to do is set up some template parts and template visual states. Template parts are used identify the types of the named parts that are used to apply control templates. If that line makes little sense, you can blame it on inability to express the concept  It will become clearer exactly what purpose template parts serve as we move forward. Template visual states on the other hand are a way for the template to identify transitions from one state of the control to the other. A button for example will have visual states for events like “mouse over”, “mouse down”, “disabled” etc. Template visual states are a way for the template writer to specify how each of these states should look visually.

    For our control we will need a template part that identifies the control that can be clicked to expand or collapse the panel. We will also need the template to identify a container control (such as a Grid or another Panel) that contains the content. Finally, we will need a control that contains the content container. We will see why this is needed a little later.

    We only need two visual states, one to represent the expanded state and the other to represent the collapsed state. Visual states can also be grouped. For example, you might want to have one set of visual states that specify what a control’s “mouse over” and “mouse down” events look like when it is enabled. On the other hand, you might want another set of visual states that specify what “mouse over” and “mouse down” look like when the control is disabled. This is where visual state groups come in handy. For our purposes, we’ll just stick with one group we’ll call CommonStates. Enough talk, now let’s look at some code:

    [TemplatePart(Name=CollapsiblePanel.ExpandCollapseButton, Type=typeof(FrameworkElement))]
    [TemplatePart(Name = CollapsiblePanel.ContentContainer, Type = typeof(Panel))]
    [TemplatePart(Name = CollapsiblePanel.PanelContent, Type = typeof(FrameworkElement))]
    [TemplateVisualState(GroupName=CollapsiblePanel.CommonStates, Name=CollapsiblePanel.Expand)]
    [TemplateVisualState(GroupName = CollapsiblePanel.CommonStates, Name = CollapsiblePanel.Collapse)]
    public class CollapsiblePanel : ContentControl
    {
    private const string ExpandCollapseButton = "ExpandCollapseButton";
    private const string ContentContainer = "ContentContainer";
          private const string PanelContent = "PanelContent";
          private const string CommonStates = "CommonStates";
          private const string Collapse = "Collapse";
          private const string Expand = "Expand";

          private string _RollUpStoryboardName = null;
          private string _RollDownStoryboardName = null;
          private FrameworkElement _expandCollapseButton;
          private Panel _contentContainer;
          private FrameworkElement _content;

          public CollapsiblePanel()
          {
           DefaultStyleKey = typeof(CollapsiblePanel);
          }
    }

    Notice how the types of each of the template parts is specified alongside the part’s name. This makes it easy for someone who writes controls to guide the template writer. Be careful not to make the type too restrictive lest you limit the creativity of template writers. Notice for instance how I chose to make the ExpandCollapseButton a FrameworkElement instead of a button. This is because in Silverlight practically all controls and shapes have events for detecting mouse clicks and I did not want to be limited to using buttons for this template part.
    Before we go any further I thought I’d discuss how the collapse/expand mechanism would work. When collapsing the panel we want the contents of the panel to disappear. However, rather than scaling the content to a zero size, I wanted to give the impression that it was rolling up behind a title bar. My solution for this was to put all the content inside a container control and set the clipping rectangle of the container control to be the same width/height as the content. This part of the template will be represented by the ContentContainer TemplatePart defined above.

    Another container control inside ContentContainer would contain the actual content. This is the TemplatePart called PanelContent. When I want the content to roll up, I simply translate PanelContent up on the Y axis so that all the content ends up outside of the clipping region of ContentContainer. The following illustration represents how this works.

    How it works

    The gray dashed lines above represent the content when it moves out of the clipping region of the ContentContainer.
    Now lets take a look at the OnApplyTemplate method implementation for CollapsiblePanel. The OnApplyTemplate method is a virtual method of ContentControl that is called as soon as the template is applied. This is a good place to get references to the template parts and set up any events that we might want to listen for. Here’s what the code looks like:


    public override void OnApplyTemplate()
    {
           base.OnApplyTemplate();

          _expandCollapseButton = GetTemplateChild(ExpandCollapseButton) as FrameworkElement;
          _contentContainer = GetTemplateChild(ContentContainer) as Panel;
          _content = GetTemplateChild(PanelContent) as FrameworkElement;

          if (_contentContainer != null)
          {
           _contentContainer.SizeChanged += new SizeChangedEventHandler(_contentContainer_SizeChanged);
          }

          if (_content != null)
          {
           _content.SizeChanged += new SizeChangedEventHandler(_content_SizeChanged);
          }

          if (_expandCollapseButton != null)
          {
           if (_expandCollapseButton is ButtonBase)
                {
                 (_expandCollapseButton as ButtonBase).Click += new RoutedEventHandler(_expandCollapseButton_Click);
                }
                else
                {
                 _expandCollapseButton.MouseLeftButtonUp += new MouseButtonEventHandler(_expandCollapseButton_MouseLeftButtonUp);
                }
           }           
    }

    First, we call the base implementation of OnApplyTemplate. Then, we get references to all the template parts using the GetTemplateChild method. We set size changed event handlers for both the content container and the content itself. This is needed so that we can set up the clipping rectangle (on the content container) and the translation animation for the content that I talked about earlier. Finally, we set up mouse events for the expand/collapse button. Notice that we use the Click event if the expand/collapse control inherits from ButtonBase, otherwise we just use the MouseLeftButtonUp event. This is important because controls that inherit from ButtonBase do not let the MouseLeftButtonUp event bubble up. The Click event is where you listen for mouse clicks in this case.

    Lets take a look at the implementation of the events defined in OnApplyTemplate to get a better idea of what goes on when each of these events is triggered. The _contentcontainer_SizeChanged event handler simply sets up a clipping rectangle that is the same width/height as the content container:

    void _contentContainer_SizeChanged(object sender, SizeChangedEventArgs e)
    {
          Panel container = sender as Panel;
          if (container != null)
          {
                RectangleGeometry rg = new RectangleGeometry();
                Rect r = new Rect(0, 0, container.ActualWidth, container.ActualHeight);
                RectangleGeometry clip = new RectangleGeometry();
                rg.Rect = r;
                container.Clip = rg;
           }
    }

    The _content_SizeChanged event handler is used to set up storyboards that actually show the content rolling up and down (depending upon whether it is expanding or collapsing). Here’s the code:

    void _content_SizeChanged(object sender, SizeChangedEventArgs e)
    {
           FrameworkElement content = sender as FrameworkElement;
          if (content != null)
          {
                TransformGroup tGroup = new TransformGroup();
                TranslateTransform translate = new TranslateTransform();
                translate.SetValue(FrameworkElement.NameProperty, "RollTransform" + Guid.NewGuid().ToString());
                translate.Y = 0;
                tGroup.Children.Add(translate);
                content.RenderTransform = tGroup;

                _RollUpStoryboardName = "RollUp" + Guid.NewGuid().ToString();
                _RollDownStoryboardName = "RollDown" + Guid.NewGuid().ToString();

                _SetupYTranslationStoryboard(translate, _RollUpStoryboardName, -content.ActualHeight);
                _SetupYTranslationStoryboard(translate, _RollDownStoryboardName, 0); }
    }

    The method first attaches a translate transform to the content’s RenderTransform. The _SetupYTranslationStoryboard method is used to set up the actual storyboard that will translate the content up or down. The method takes a reference to the translate transform, a string containing the name of the storyboard and the value of Y to which the storyboard must animate the transform. To roll up, we simply translate up by the content’s height (negative of the content.ActualHeight) and to roll down we translate Y back to 0. Lets take a look at the _SetupYTranslationStoryboard method:

    void _SetupYTranslationStoryboard(TranslateTransform transform, string sbName, double translation)
    {
          if (Resources.Contains(sbName))
          {
                Storyboard sb = Resources[sbName] as Storyboard;
                DoubleAnimationUsingKeyFrames anim = sb.Children[0] as DoubleAnimationUsingKeyFrames;
                SplineDoubleKeyFrame keyFrame = anim.KeyFrames[0] as SplineDoubleKeyFrame;
                keyFrame.Value = translation;
          }
          else
          {
                Storyboard sb = new Storyboard();
                sb.SetValue(NameProperty, sbName);
                DoubleAnimationUsingKeyFrames anim = new DoubleAnimationUsingKeyFrames();
                sb.Children.Add(anim);
                Storyboard.SetTarget(anim, transform);
                Storyboard.SetTargetProperty(anim, new PropertyPath("Y"));
                anim.BeginTime = new TimeSpan(0, 0, 0);
                SplineDoubleKeyFrame keyFrame = new SplineDoubleKeyFrame();
                KeySpline spline = new KeySpline();
                spline.ControlPoint1 = new Point(0, 1);
                spline.ControlPoint2 = new Point(1, 1);
                keyFrame.KeySpline = spline;
                keyFrame.KeyTime = new TimeSpan(0, 0, 1);
                keyFrame.Value = translation;
                anim.KeyFrames.Add(keyFrame);
                Resources.Add(sbName, sb);
          }
    }

    I like smooth animations, so I use a key spline to slow down the animation towards the end. Now let’s take a look at what the expand/collapse button’s event handlers look like:

    void _expandCollapseButton_MouseLeftButtonUp(object sender, MouseButtonEventArgs e)
    {
        IsExpanded = !IsExpanded;
    }

    void _expandCollapseButton_Click(object sender, RoutedEventArgs e)
    {
        IsExpanded = !IsExpanded;
    }


    These event handlers simply flip the value of the IsExpanded dependancy property (which we will look at in a moment). All the real work of animating between the expanded and contracted states goes on in the property changed callback of the IsExpanded property. Let’s take a look at how IsExpanded is defined:


    public bool IsExpanded
    {
          get { return (bool)GetValue(IsExpandedProperty); }
          set { SetValue(IsExpandedProperty, value); }
    }

    // Using a DependencyProperty as the backing store for IsExpanded.  This enables animation, styling, binding, etc...
    public static readonly DependencyProperty IsExpandedProperty =
                DependencyProperty.Register("IsExpanded", typeof(bool), typeof(CollapsiblePanel), new PropertyMetadata(new PropertyChangedCallback(IsExpandedChanged)));

    private static void IsExpandedChanged(DependencyObject o, DependencyPropertyChangedEventArgs e)
    {
          bool expanded = (bool)e.NewValue;
          CollapsiblePanel panel = o as CollapsiblePanel;

          if (expanded)
          {
                VisualStateManager.GoToState((o as Control), Expand, true);
                if (panel != null && panel._RollDownStoryboardName != null)
                {
                 Storyboard sb = (o as FrameworkElement).Resources[panel._RollDownStoryboardName] as Storyboard;
                        if (sb != null)
                        {
                         sb.Begin(); 
                        }
                }
           }
         else
         {
                VisualStateManager.GoToState((o as Control), Collapse, true);
                if (panel != null && panel._RollUpStoryboardName != null)
                {
                   Storyboard sb = (o as FrameworkElement).Resources[panel._RollUpStoryboardName] as Storyboard;
                   if (sb != null)
                   {
                      sb.Begin();
                   }
                }            
          }
    }

    Well, I did say “real work” but our new friend the VisualStateManager makes going from one state to the other a snap! As you can see, the IsExpanded property is a boilerplate dependancy property. If the new value is true, the property changed callback simply calls the VisualStateManager to go to the Expand state and runs the RollDown storyboard defined by _content_SizeChanged earlier. If the new value is false, the property changed callback does the opposite. Easy! :)

    We will also be needing another dependancy property to show a title in the panel’s title bar. Here’s the code for the Title property:

    public string Title
    {
    get { return (string)GetValue(TitleProperty); }
          set { SetValue(TitleProperty, value); }
    }

    // Using a DependencyProperty as the backing store for Title.  This enables animation, styling, binding, etc...
    public static readonly DependencyProperty TitleProperty =
                DependencyProperty.Register("Title", typeof(string), typeof(CollapsiblePanel), null);

     


    The last (and some might say, most significant) bit is setting up a default template for our CollapsiblePanel control. Here’s what the XAML looks like:

    <ResourceDictionary
      xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"   
      xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
      xmlns:local="clr-namespace:Knowledgecast.Controls"
      xmlns:vsm="clr-namespace:System.Windows;assembly=System.Windows"
      >

        <vsm:Style TargetType="local:CollapsiblePanel">
            <vsm:Setter Property="Template">
                <vsm:Setter.Value>
                    <ControlTemplate TargetType="local:CollapsiblePanel">
                        <Grid>                    
                            <vsm:VisualStateManager.VisualStateGroups>                           
                                <vsm:VisualStateGroup x:Name="CommonStates">
                                    <vsm:VisualState x:Name="Collapse">
                                        <Storyboard>
                                            <DoubleAnimationUsingKeyFrames Storyboard.TargetName="ArrowAngleTransform" Storyboard.TargetProperty="Angle" BeginTime="00:00:00">
                                                <SplineDoubleKeyFrame KeyTime="00:00:01" Value="0">
                                                    <SplineDoubleKeyFrame.KeySpline>
                                                        <KeySpline ControlPoint1="0,1" ControlPoint2="1,1"/>
                                                    </SplineDoubleKeyFrame.KeySpline>
                                                </SplineDoubleKeyFrame>
                                            </DoubleAnimationUsingKeyFrames>
                                        </Storyboard>
                                    </vsm:VisualState>
                                    <vsm:VisualState x:Name="Expand">
                                        <Storyboard>
                                            <DoubleAnimationUsingKeyFrames Storyboard.TargetName="ArrowAngleTransform" Storyboard.TargetProperty="Angle" BeginTime="00:00:00">
                                                <SplineDoubleKeyFrame KeyTime="00:00:01" Value="90">
                                                    <SplineDoubleKeyFrame.KeySpline>
                                                        <KeySpline ControlPoint1="0,1" ControlPoint2="1,1"/>
                                                    </SplineDoubleKeyFrame.KeySpline>
                                                </SplineDoubleKeyFrame>
                                            </DoubleAnimationUsingKeyFrames>
                                        </Storyboard>
                                    </vsm:VisualState>
                                </vsm:VisualStateGroup>
                            </vsm:VisualStateManager.VisualStateGroups>
                            <Grid.RowDefinitions>
                                <RowDefinition Height="20" />
                                <RowDefinition />
                            </Grid.RowDefinitions>
                            <Grid x:Name="ExpandCollapseButton" Height="20" Grid.Row="0">
                                <Path x:Name="TitleBack" Opacity="0.8" HorizontalAlignment="Stretch" Margin="0,0,0,0" VerticalAlignment="Stretch" Stretch="Fill" StrokeThickness="0.5" Data="M12.5,7 C47.333332,7 115.85664,7 117,7 C118.14336,7 122.1255,6.7291665 122.25,12 C122.3745,17.270834 122.25,18.333334 122.25,21.5 L12.5,21.5 z">
                                    <Path.Fill>
                                        <RadialGradientBrush GradientOrigin="0.699000000953674,0.792999982833862">
                                            <RadialGradientBrush.RelativeTransform>
                                                <TransformGroup>
                                                    <ScaleTransform CenterX="0.5" CenterY="0.5" ScaleX="1.4" ScaleY="2.188"/>
                                                    <SkewTransform CenterX="0.5" CenterY="0.5"/>
                                                    <RotateTransform CenterX="0.5" CenterY="0.5"/>
                                                    <TranslateTransform X="0.017" Y="0.009"/>
                                                </TransformGroup>
                                            </RadialGradientBrush.RelativeTransform>
                                            <GradientStop Color="#FF00008B" Offset="1"/>
                                            <GradientStop Color="#FFADD8E6" Offset="0"/>
                                        </RadialGradientBrush>
                                    </Path.Fill>
                                </Path>
                                <TextBlock Cursor="Arrow" HorizontalAlignment="Stretch" Margin="27.75,2.75,-5,1.75" VerticalAlignment="Stretch" FontFamily="Verdana" FontSize="11" FontStyle='Normal' FontWeight='Normal' Foreground='#FFFFFFFF' Text='{TemplateBinding Title}' Opacity='1' x:Name='Title'/>
                          <Path x:Name="Arrow" HorizontalAlignment="Left" Margin="5.85300016403198,2.81200003623962,0,3.29800009727478" VerticalAlignment="Stretch" Width="10.685" Fill="#FFFFFFFF" Stretch="Fill" Stroke="#FF000000" StrokeThickness="0" Data="M182.75038,211.50015 L216.5,234.50017 L182.81238,257.87216 z" RenderTransformOrigin="0.5,0.5">
                           <Path.RenderTransform>
                            <TransformGroup>
                             <RotateTransform x:Name="ArrowAngleTransform" Angle="90"/>
                            </TransformGroup>
                           </Path.RenderTransform>
                          </Path>  
                      </Grid>
                        <Grid x:Name="ContentContainer" Grid.Row="1">
                            <Border x:Name="PanelContent" BorderThickness="0">
                                <Grid>
                                        <Rectangle Opacity="0.6" Stroke="Gray" HorizontalAlignment="Stretch" VerticalAlignment="Stretch"/>
                                        <Rectangle Opacity="0.6" Stroke="Black" HorizontalAlignment="Stretch" VerticalAlignment="Stretch">
                                            <Rectangle.RenderTransform>
                                                <TranslateTransform X="-1" Y="-1" />
                                            </Rectangle.RenderTransform>
                                        </Rectangle>
                                        <ContentPresenter />
                                </Grid>
                            </Border>
                        </Grid>
                        </Grid>
                    </ControlTemplate>
                </vsm:Setter.Value>
            </vsm:Setter>
        </vsm:Style>
    </ResourceDictionary>

    Pay close attention to where each of the template parts and visual states appear. Let’s take this from the top. The expand and contract visual states define storyboards that manipulate the angle of a rotate transform. If you scroll down and look at a Path element called Arrow defined in the ExpandCollapseButton you will see that this angle transform belongs to that path. The Path is simply an arrow that is rendered inside the expand/collapse title bar.

    The whole layout is defined inside a Grid container. The ExpandCollapseButton is in fact a Grid which sits in the first row of the container grid. The ContentContainer (below the ExpandCollapseButton) is another Grid that sits in the second row. At runtime, we will be applying a clipping rectangle to the ContentContainer grid. The PanelContent is a Border container. Aside from a couple of rectangles for visual effect, it also contains a ContentPresenter control. The ContentPresenter is the heart of a ContentControl. This is where all the content that the user provides for this control goes.

    To set this up as the default template for our control we are going to put the XAML in a file called generic.xaml that is part of the project that contains the control class. We will also set the Build Action on this file to "Page" and Custom Tool to "MSBuild:MarkupCompilePass1". This should tell Silverlight to render the control using this template when none is specified by the consumer.

    To close, lets take a look at a very simple example of the CollapsiblePanel’s use:

    <UserControl x:Class="Knowledgecast.DemoApplication.Page"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:knowledgecast="clr-namespace:Knowledgecast.Controls;assembly=Knowledgecast.Controls"
        Width="220" Height="200">
        <Grid x:Name="LayoutRoot" Background="White">
            <knowledgecast:CollapsiblePanel Margin="10,10,10,10" Title="Collapse/Expand" FontSize="10">
                <Grid>
                    <StackPanel Margin="4,2,2,2" Orientation="Vertical">
                        <TextBlock Text="This is a content control" />
                        <TextBlock Text="It can contain anything" />
                        <TextBox Width="100" Margin="0,10,0,10" Text="a textbox" />
                        <RadioButton Width="100" Content="a radio button" />
                        <TextBlock Text="Or that graphic in the background" />                   
                        <TextBlock Margin="0,20,0,0" Text="Click the title bar to expand/collapse" />
                    </StackPanel>
                </Grid>
            </knowledgecast:CollapsiblePanel>
        </Grid>
    </UserControl>

    All the code for this post along with examples is available here:

    The next post in this series which explains the Visual Transitions used in the collapsible panel is here.

    More Posts Next page »
    Page view tracker