Welcome to MSDN Blogs Sign in | Join | Help

Knowledgecast

Interesting solutions to interesting problems
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

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: 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 inh