Disclaimer: It has come to my attention that there is some confusion in the community regarding the "official status" of the WPF Starter Kit. I wanted to make it clear that this toolkit has come out of my and my team's efforts on a recent project and is by no means the endorsed (by Microsoft) or only way of implementing the features outlines below. The p&p team's Composite WPF and Silverlight guidance (http://compositewpf.codeplex.com/) and the WPF codeplex site (http://wpf.codeplex.com) are the official sources of guidance. Another source of inspiration to me has been the people and articles of http://wpfdisciples.wordpress.com/. There are a bunch of community-created MVVM toolkits out there, and most are as valid as this one.
If you're looking to get straight to the code, go here: http://wpfstarterkit.codeplex.com/
I have been working on a WPF project the last few months. We've managed to accomplish something pretty incredible by putting a WPF application at the center of a hostile environment (a train engine) paired with a ruggedized tablet. The tablet itself ships with Windows XP Tablet Edition (talk about being behind the times!) but the great thing about WPF as a platform is that this didn't matter so much. We were still able to build our application and deploy it on the tablet as well as on Windows 7 desktops with only minor issues (most of them to do with the graphic drivers on different video hardware).
When I started out with this application I really wanted to use the Model View View Model pattern. Not only because MVVM is the hot new pattern (which it admittedly is) but also because I think MVVM is what MVC always wanted to be. Of course, WPF's brilliant data binding and commanding support only helps. We did evaluate what was then known as Prism (http://compositewpf.codeplex.com/) but found it a little too complicated for a project this size (the application only has about 30 views). Another thing that didn't fit was the large learning curve. Most of the people on the project, while quite well versed with managed code, were new to the WPF way of doing things.
That was when I made the decision to build a up a barebones, simple MVVM/Navigation/Commanding framework that would be easy to pick up in a single afternoon. While it does not have the modularity, complexity or extensibility of Prism, it worked very well for our application. Here's what the WPF Starter Kit supports:
A simple, easy to use implementation of the Model View View Model pattern
- A navigation framework that allows for clean navigation between View/ViewModel units. This framework is based on built-in WPF navigation
- Support for passing data between View Models
- Support for generic exception handling
- Support for long running operations on the UI thread
The source code for the project is here: http://wpfstarterkit.codeplex.com/. Feel free to log bugs if you find something not working. There's also a presentation that should make it easy to get started using the kit: http://wpfstarterkit.codeplex.com/Release/ProjectReleases.aspx?ReleaseId=35301#DownloadId=90586. Finally, there is a sample project in the solution that shows off each of the features mentioned above. I'll try and make separate posts covering each of these features and tasks you'd normally need to perform while writing a functional multi-view application with the WPF Starter Kit.
Have fun!
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! :)
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.
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!
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 :)
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.
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 :)
Here's the source code:
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 :)
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.
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.