Last week I wrote a blog post showing how you can write inline code in XAML. When that blog post was published, @ElegantCode tweeted the post on Twitter. Rob Relyea soon re-tweeted that post with a link to one of his own posts on exploring new techniques for DLR script in XAML which also links to another one of his posts which shows different techniques for hooking up events in compiled and uncompiled scenarios which in turn links to this post about embedding DLR scripts in XAML written by Daniel Paull. Wow, that is a lot of linking.
Well, now that everyone has been given their respective credit, let’s continue. I wanted to take my last post a little further. In that post I simply created an event handler for a button click event using inline code in the x:Code directive. If you started to play with the sample code, you might have thought to yourself, “I wonder if I can load this XAML, event handler and all, from a file or database and have it still work.”. Well, if you tried it, you found the answer really quick. No! The reason is pretty simple; code must be compiled in order to execute. Therefore any code inside loose XAML will fail. Also, when using loose XAML, trying to hookup an event handler would crash your application because it is not allowed.
Oh, then it must not be possible? Wrong! Well, then how do you do it if the code must be compiled? Simple, we compile the code just before we run it. Say what? That’s right, let me introduce you to the CodeDom. The CodeDom allows you to dynamically generate and compile code at runtime. Let’s get to it.
We need to attach event handlers to elements in loose XAML, and have them execute C# code. Each element has different events to handle, and each event will execute different code. This tells me that we need an object that contains two properties, one to specify the RoutedEvent to handle, and another to specify the c# code to execute. We also need a common event handler definition to execute whenever the RoutedEvent is invoked.
{
///<summary>
/// Gets or sets the routed event to execute.
///</summary>
///<value>The routed event.</value>
public RoutedEvent RoutedEvent { get; set; } ///<summary>
/// Gets or sets the script to run when the RoutedEvent is executed.
///</summary>
///<value>The script.</value>
public string Script { get; set; }///<summary>
/// Called when the RoutedEvent is executed.
///</summary>
///<param name=”sender”>The sender.</param>
///<param name=”e”>The <see cref=”System.Windows.RoutedEventArgs”/> instance containing the event data.</param>
private void OnEventExecuted(object sender, RoutedEventArgs e)
{
}
}
That was simple enough. Now, in order to attach event handlers to elements in loose XAML, we need away to get around the “no event handlers allowed” limitation. To do this we can use an attached property.
public static DynamicEvent GetHandler(DependencyObject obj)
{
return (DynamicEvent)obj.GetValue(HandlerProperty);
}
public static void SetHandler(DependencyObject obj, bool value)
{
obj.SetValue(HandlerProperty, value);
}private static void OnHandlerChanged(DependencyObject o, DependencyPropertyChangedEventArgs e)
{
UIElement uie = o as UIElement;
if (uie == null)
throw new Exception(“Attempt to set EventHandler on non-UIElement Type”);DynamicEvent oldHandler = e.OldValue as DynamicEvent;
DynamicEvent newHandler = e.NewValue as DynamicEvent;
// unhook the old event
if (oldHandler != null)
uie.RemoveHandler(oldHandler.RoutedEvent, new RoutedEventHandler(oldHandler.OnEventExecuted));
// hook up the new
if (newHandler != null)
uie.AddHandler(newHandler.RoutedEvent, new RoutedEventHandler(newHandler.OnEventExecuted));
}
This attached property allows use to attach an instance of our DynamicEvent class to our element. When the Handler attached property is set, we simple add our OnEventExecute event handler to the RoutedEvent specified in the RoutedEvent property. Now we have the basic framework needed to attach and event handler to any event on any element in loose XAML. Let’s see how to attach an event handler in XAML using this newly created DynamicEvent class.
<local:DynamicEvent.Handler>
<local:DynamicEvent RoutedEvent=”Button.Click”>
<local:DynamicEvent.Script>
<sys:String xml:space=”preserve”>
<![CDATA[
var button = sender as Button;
button.Content = “Changed with Dynamic Code”;
]]>
</sys:String>
</local:DynamicEvent.Script>
</local:DynamicEvent>
</local:DynamicEvent.Handler>
</Button>
As you can see I have added a handler for a Button’s Click event. I also provided a Script to execute when the event is invoked. Notice that we are preserving the whitespace in the script. Of course nothing will happen just yet. Now we need to take the script and compile it at run time. This is where the CodeDom comes in.
The steps necessary to compile code at run time go a little something like this:
- Create a CodeCompileUnit
- Add our namespace and import statements
- create our dynamic class
- add a method that will execute the Script
- compile the code
- execute the code
All these steps must be performed when the ROutedEvent is invoked, which means the code must be compiled inside the OnEventExecuted event handler.
{
CodeCompileUnit compileUnit = new CodeCompileUnit(); //create our namesapce and import statements, then add it to the compile unit
CodeNamespace namespaces = CreateNamespaceAndImports();
compileUnit.Namespaces.Add(namespaces);//create our target class and add it to the namespace
CodeTypeDeclaration targetClass = CreateTargetClass();
namespaces.Types.Add(targetClass);
//create the ExecuteScript method and then add it to our class
CodeMemberMethod executeEventMethod = CreateExecuteScriptMethod();
targetClass.Members.Add(executeEventMethod);
//create our compiler parameters
CompilerParameters compilerParams = CreateCompilerParameters();
//compile the assembly
var results = CompileAssembly(compileUnit, compilerParams);
//execute the code
ExecuteCode(results, sender, e);
}
The first step is really easy. We simply need to create an instance of the CodeCompileUnit class. The CodeCompileUnit references the CodeDom object graph and has properties for storing attributes, namespaces, and assemblies. Next we need to create our namespace and import statements.
/// Creates the namespace and imports.
///</summary>
///<remarks>I have only added three import statements as an example. You should add all import statements required to run your dynamic code</remarks>
private static CodeNamespace CreateNamespaceAndImports()
{
CodeNamespace namespaces = new CodeNamespace(“WPFCodeInjection”);
namespaces.Imports.Add(new CodeNamespaceImport(“System”));
namespaces.Imports.Add(new CodeNamespaceImport(“System.Windows”));
namespaces.Imports.Add(new CodeNamespaceImport(“System.Windows.Controls”));
return namespaces;
}
Once the namespace and import statement are created we need to add them to out CodeCompileUnit instance. Then we need to create our dynamic class. We need to make sure it is a public class:
/// Creates the target class.
///</summary>
private static CodeTypeDeclaration CreateTargetClass()
{
CodeTypeDeclaration targetClass = new CodeTypeDeclaration(“DynamicClass”);
targetClass.TypeAttributes = TypeAttributes.Public;
return targetClass;
}
When the dynamic class is created we add it to our namespace. Now we create a method. We give it a name of ExecuteScript and set its access modifier to public. We specify that it accepts two parameters. One being the sender which is of type object, and one that is the event args that is of type RoutedEventArgs. Lastly, we set the body of the method to the Script property.
/// Creates the ExecuteScript method.
///</summary>
private CodeMemberMethod CreateExecuteScriptMethod()
{
CodeMemberMethod executeEventMethod = new CodeMemberMethod();
executeEventMethod.Name = “ExecuteScript”;
executeEventMethod.Attributes = MemberAttributes.Public | MemberAttributes.Final;
executeEventMethod.Parameters.Add(new CodeParameterDeclarationExpression(“System.Object”, “sender”));
executeEventMethod.Parameters.Add(new CodeParameterDeclarationExpression(“System.Windows.RoutedEventArgs”, “e”));
executeEventMethod.Statements.Add(new CodeSnippetStatement(Script));
return executeEventMethod;
}
Now we have to create our compiler parameters. These include adding referencing to the necessary Dlls required to run your code. We also want to specify that we want to generate the code in memory. If you wanted to you could save the assembly to a file.
/// Creates the compiler parameters.
///</summary>
private static CompilerParameters CreateCompilerParameters()
{
//add compiler parameters and assembly references
CompilerParameters compilerParams = new CompilerParameters();
compilerParams.CompilerOptions = “/target:library /optimize”;
compilerParams.GenerateExecutable = false;
compilerParams.GenerateInMemory = true; //you can add references individually if you want by providing the location of the assembly
//compilerParams.ReferencedAssemblies.Add(“System.dll”);//I am lazy so I will just loop through the assemblies that are already loaded and add those
foreach (Assembly assembly in AppDomain.CurrentDomain.GetAssemblies())
{
try
{
string location = assembly.Location;
if (!String.IsNullOrEmpty(location))
compilerParams.ReferencedAssemblies.Add(location);
}
catch (NotSupportedException)
{
// this happens for dynamic assemblies, so just ignore it.
}
}
return compilerParams;
}
Now we compile the code into an assembly.
/// Compiles the assembly.
///</summary>
///<param name=”compileUnit”>The compile unit.</param>
///<param name=”compilerParams”>The compiler params.</param>
private static CompilerResults CompileAssembly(CodeCompileUnit compileUnit, CompilerParameters compilerParams)
{
using (CSharpCodeProvider provider = new CSharpCodeProvider())
{
// =============================================================================== //
// if you want to see the actual complied source code uncomment //
// =============================================================================== // //StringBuilder source = new StringBuilder();
//StringWriter sw = new StringWriter(source);
//provider.GenerateCodeFromCompileUnit(compileUnit, sw, new CodeGeneratorOptions());
//string sourceCode = source.ToString();var results = provider.CompileAssemblyFromDom(compilerParams, compileUnit);
return results;
}
}
The last thing we need to do is run the code that was just compiled. We will need the help of reflection to accomplish this.
/// Executes the ExecuteScript method from the compiled CodeDom assembly.
///</summary>
///<param name=”results”>The results.</param>
///<param name=”sender”>The sender.</param>
///<param name=”e”>The <see cref=”System.Windows.RoutedEventArgs”/> instance containing the event data.</param>
private void ExecuteCode(CompilerResults results, object sender, RoutedEventArgs e)
{
try
{
Assembly executingAssembly = results.CompiledAssembly;
if (executingAssembly != null)
{
//create an instance of our newly compiled assembly
object assemblyInstance = executingAssembly.CreateInstance(“WPFCodeInjection.DynamicClass”); //get our ExecuteScript method and execute it.
MethodInfo info = assemblyInstance.GetType().GetMethod(“ExecuteScript”);
info.Invoke(assemblyInstance, new object[] { sender, e });
}
}
catch (Exception ex)
{
Console.WriteLine(“Error: An exception occurred while executing the script”, ex);
}
}
That is it. Now to test this out lets create a .txt file that contains our XAML that we define above and save it CodeSnippet.txt. In our MainWindow.xaml.cs, lets handle the Loaded event and load our CodeSnippet.txt using the XamlReader.Load method.
{
using (FileStream fs = new FileStream(@”./CodeSnippet.txt”, FileMode.Open))
{
this.Content = XamlReader.Load(fs);
}
}
Run the application is this is what it should look like.
Now click the button and our C# code defined within the Script property will be dynamically compiled and executed changed the text of the button to this:
This is a very simple example and is not intended to be a complete solution, but it does show some of the powerful capabilities you have using WPF, XAML, and the CodeDom. Like always download the source and start playing around. See what kind of cool stuff you can do with it.
How about multiple events for the same control ? I tried to assign Button.Click and Button.MouseMove and it said Handler property already assigned which seems fair as I have assigned the property to Click event. But how to work around for this ? Thank you for the help.
Sorry, but I really don’t have time to invest in researching this. Hopefully you have already found your solution. If so, please share it.