Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How is XAML interpreted and executed at runtime?

Tags:

.net

wpf

xaml

What happens internally between loading XAML (or BAML) and receiving the root object (Window, for example)?

What first pops up in my mind is that Reflecton is used to create objects, set their properties and so on. But maybe I am wrong?

Maybe someone can explain how XAML/BAML is parsed and executed at runtime or give a link to a good article with the explanation?

To make my question a little bit more clear, let's discuss the short example:

<Button Margin="10">OK</Button>

So, the parser sees that a Button object needs to be created, that its Margin property has to be set to 10 and its content has to be set to "OK". How is that done? By using Reflection (plus TypeConverters, etc)?

like image 871
WpfNewbie Avatar asked Dec 11 '13 20:12

WpfNewbie


People also ask

How does WPF XAML work?

The WPF markup compiler will create a partial class for any compiled XAML file, by deriving a class from the root element type. When you provide code-behind that also defines the same partial class, the resulting code is combined within the same namespace and class of the compiled app.

What is XAML briefly explain?

XAML stands for Extensible Application Markup Language. It's a simple and declarative language based on XML. In XAML, it very easy to create, initialize, and set properties of objects with hierarchical relations.

Is XAML a programming language?

XAML is a new descriptive programming language developed by Microsoft to write user interfaces for next-generation managed applications. XAML is the language to build user interfaces for Windows and Mobile applications that use Windows Presentation Foundation (WPF), UWP, and Xamarin Forms.


1 Answers

You may notice in your .xaml.cs files that the classes backing your compiled XAML files are marked as partial classes. A XAML build task generates a second .cs file with another partial class section containing an implementation of the IComponentConnector.InitializeComponent() method, which is called by the default constructor in the code behind. This method basically runs through the XAML (which is actually in BAML form at this point) and uses it to "fix up" the newly created object, as opposed to creating a new object from the XAML source, which is what would happen if you were to use a XamlReader to load or parse an object.

So, when you instantiate a new compiled XAML object (e.g., a UserControl), whatever code precedes the call to InitializeComponent() in the constructor will execute. Then, all the properties and event handlers set in the XAML file will be processed during the call to InitializeComponent(), after which the constructor resumes. This can be useful to know, as you may want to ensure that certain properties get set before or after the XAML file is processed.

As to how the XAML is parsed, it is essentially read as a stream of XAML nodes representing property assignments, object declarations, etc., which are performed in order by the services in System.Xaml. This node stream is based on a common object model, which may have been constructed from a BAML stream, an XML document (e.g., a loose .xaml file), another object instance, etc. BAML, being more compact than the XML-based format, is generally faster to parse.


Addendum: In the example you added, you ask how the parser sees that a Button object needs to be created and the Margin set. The short answer is: it depends. Specifically, it depends on the schema context used to read the XAML stream.

The XAML parser uses its own type system, of which there are at least two implementations:

  1. A standard CLR type system based on reflection and System.ComponentModel;
  2. A WPF type system that extends #1 by including special support for dependency properties and routed events.

This is, based on my recollection of the XAML Language Spec, approximately what happens:

  1. The XAML parser encounters a StartObject node for the Button type, which (for the standard WPF namespace mappings) resolves to System.Windows.Controls.Button. This tells the parser it needs to create an instance of a Button object, which it does by invoking its default constructor via reflection.
  2. The next node in the stream is a StartMember node, with a member name of Margin. The type model for the WPF schema context will resolve this to the Margin dependency property.
  3. A Value node comes next, which tells the parser to set a value of "10" (a string). The parser sees that the property type Thickness is incompatible with the string value. It consults its type system to see if a [ValueSerializer] attribute exists on the Margin property, which could be used to convert the string; no such attribute exists. It checks the property for a [TypeConverter] attribute; again, it finds none. It looks for a [TypeConverter] attribute on the Thickness type itself and finds one, which instructs it to use a ThicknessConverter to convert the string value into a Thickness. It does so. Since Margin is a dependency property, it uses the SetValue() API to set the property value; if it were a CLR property, it would use reflection or a PropertyDescriptor.
  4. An EndMember node tells the parser to end the property assignment.
  5. The parser encounters a Value node with the content "OK". The parser knows it is constructing a complex object, so the content cannot represent the entire object. It looks for a [ContentProperty] attribute on Button and its supertypes; it finds one on ContentControl, which indicates that the value should be used to set the Content property (which gets resolved to the corresponding dependency property). Content is an object, so it assigns the string value directly (again, using SetValue()).
  6. The next node is EndObject, which tells the parser it has finished processing the Button object.

Note that I used the term "parser" to simplify things. Truthfully, none of this happens in the parsing stage (if a "parsing" stage even exists). What you may think of as the "parsing" stage is just the construction of a stream of XAML nodes. The creation and/or population of the declared object(s) actually happens by feeding that stream into a XamlObjectWriter, which is simply the implementation of XamlWriter which writes XAML nodes to an object (as opposed to an XML document or a BAML stream). At the high level, there are only two things happening:

  1. XamlReader transforms something into stream of XAML nodes.
  2. XamlWriter transforms a stream of XAML nodes into something.

In the case of a compled XAML resource, a compile-time build task pipes the output of a XamlXmlReader into a BamlWriter to "compile" the XAML. At runtime, the input of a BamlReader is piped into an XamlObjectWriter to create or "fix up" the root object.

Once you understand all of this, you may start to recognize XAML as a powerful serialization and persistence format, as opposed to merely a language for building UIs.

like image 169
Mike Strobel Avatar answered Sep 22 '22 21:09

Mike Strobel