Dynamically create WPF UI that responds to events!

08 Oct 2008

This is kind of complex, but simple in a totally different sense. As we all know one could create WPF elements on the fly using XamlReader.Load(). Shown below is snippet of code that does exactly this.

private string xmlns = "http://schemas.microsoft.com/winfx/2006/xaml/presentation";
private string xmlns_x = "http://schemas.microsoft.com/winfx/2006/xaml";

private object GetUIFromXaml()
{
XmlDocument xdoc = GetXamlDoc();

///
Now we are done with modifications, so we need an XmlReader. In this case, we use a XmlTextReader
///
We build a StringReader on the updated xml.
XmlTextReader xmlReader = new XmlTextReader(new StringReader(xdoc.OuterXml));

///
The above code is all the ground work needed to successfully load Xaml at runtime.
///
The XamlReader.Load() does the trick here. It compiles the Xaml into BAML and then builds the Object graph.
///
At the sametime, both the Visual Tree as well as the Logical Tree is constructed.
return XamlReader.Load(xmlReader);
}

private XmlDocument GetXamlDoc()
{
///
Xaml is XML. So we load the XAML as an XmlDocument.
///
The reason XmlDocument is used instead of XDocument is that we would like to add namespaces
///
so that the parsing is performed without issues.
XmlDocument xdoc = new XmlDocument();
xdoc.LoadXml(txtCode.Text);

///
We need to add the presentation framework namespace as well as the XAML namespace.
///This way a <Button>
like WPF objects would be properly identified.
if (string.IsNullOrEmpty(xdoc.DocumentElement.GetAttribute("xmlns"))) xdoc.DocumentElement.SetAttribute("xmlns", xmlns);
if (string.IsNullOrEmpty(xdoc.DocumentElement.GetAttribute("xmlns:x"))) xdoc.DocumentElement.SetAttribute("xmlns:x", xmlns_x);
return xdoc;
}

The GetXamlDoc method simply attaches namespaces to the XAML we wish to add. This way, even a XAML like

<TextBlock Name="txtErrors"  FontWeight="20" Text="Enter Code and Press TAB"/>

would work without issues. The XamlReader would then know where to look for TextBlock.

Now, the requirement is that we want to load buttons from XAML and attach event handler to button at "runtime". The general requirement is that we should be able to attach handlers to any WPF elements and this requires a lot of ground work which I am not showing here. So, I just simplified the problem to attach dynamic event handlers to only buttons.


//Create UI from XAML.
objectobj = GetUIFromXaml();
Buttonb = obj asButton;
if(b != null)
    AddHandlerToButton(b);

Now, the AddHandlerToButton method looks like shown.
private void AddHandlerToButton(Button b)
{
if (b.Name == "empty")
{
b.AddHandler(Button.ClickEvent, Delegate.CreateDelegate(Button.ClickEvent.HandlerType, typeof(Page1).GetMethod("b_Click",BindingFlags.Public|BindingFlags.Static), false));
}

}
So, the method shown above adds handler to the click event of the button by creating a delegate at runtime. Note that BindingFlags.Static is very important here. Otherwise, the reflection framework would try to instantiate Page1, which fails in InitializeComponent call. So make the b_Click as static such that no initialization takes place and you would successfully bind the method. Without the method being static, CreateDelegate would throw an ArgumentException saying "Error binding method to target".
The use of reflection allows one to make a completely dynamic WPF loader framework which can make use of user plugins, which defines static method handlers for WPF elements. He can then write a configuration based AddHandlerToElement method which loads the user plugin(dll) and attach the methods based on the configuration settings.
Note that you could also have done a d.Click += new RoutedEventHandler(b_Click); but this is not configurable.
Note that the XAML parsed would not allow "Click" or other events to be set within XAML. So you might want to add a separate piece of config which defines the names of the events, their handlers for a given element name. For example,
<Element name="empty" event="Click" method="b_Click" assemblyName="myassembly.dll,.,...."/>
Hopefully, this would help many who wants to make full use of dynamically created WPF forms.