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

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: