Building read-only objects in Xaml

We often use Xaml to instantiate and initialize objects. For example, given “<Foo Bar=’1’/>”, a Xaml loader creates a Foo object, and sets the Bar property to 1. That works when the Bar property is settable, but what can you do if it isn’t?

 

An example of this scenario in .Net today shows up with strings; the String type is immutable, it doesn’t have any settable properties. But sometimes you want to build a string with formats, concatenation, etc. Thus the invention of the StringBuilder class, where the properties have setters (and the methods can mutate an instance).

 

You can follow the same idea in Xaml by creating builder types, using markup extensions.

 

Here’s an example of a type whose properties can’t be set except internally, since it has read-only properties:

 

<Assembly: XmlnsDefinition("https://blogs.msdn.com/mikehillberg/BuilderPattern", "PeopleNamespace")>

Namespace PeopleNamespace

    Public Class Person

        Private _name As String

        Public ReadOnly Property Name() As String

            Get

                Return _name

            End Get

        End Property

        Private _age As Integer

        Public ReadOnly Property Age() As Integer

            Get

                Return _age

            End Get

        End Property

        Public Shared Function Create(ByVal Name As String, ByVal Age As Integer) As Person

            Dim person As New Person

            person._name = Name

            person._age = Age

            Return person

        End Function

    End Class

End Namespace

 

(I’m using the XmlnsDefinition assembly attribute here so that I can have a normal-looking Uri in my Xaml, but you can also make references directly to an assembly using the “clr-namespace” syntax, as described here.)

 

 

For demonstration, also define a List(Of Person) named “People”:

 

Namespace PeopleNamespace

    Public Class People

        Inherits List(Of Person)

    End Class

End Namespace

 

 

… and then, for example, you can’t load this Xaml, because of the read-only properties:

 

<People xmlns="https://blogs.msdn.com/mikehillberg/BuilderPattern" >

 

  <Person Name='Mike' Age='5' />

  <Person Name='Dwayne' Age='7' />

 

</People>

 

(This gives an error of “'Name' property is read-only and cannot be set from markup” ).

 

But we can create a markup extension that follows the builder pattern to create the object.

 

First, define the markup extension, named PersonExtension. It looks just like the Person type, except it has setters for the properties, and overrides the ProvideValue function:

 

<Assembly: XmlnsDefinition("https://blogs.msdn.com/mikehillberg/BuilderPatternBuilder", "PeopleBuilderNamespace")>

Namespace PeopleBuilderNamespace

    Public Class PersonExtension

        Inherits MarkupExtension

        Private _name As String

        Public Property Name() As String

            Get

                Return _name

            End Get

            Set(ByVal value As String)

                _name = value

            End Set

        End Property

        Private _age As Integer

        Public Property Age() As Integer

            Get

                Return _age

            End Get

            Set(ByVal value As Integer)

                _age = value

            End Set

        End Property

        Public Overrides Function ProvideValue(ByVal serviceProvider As IServiceProvider) As Object

            Return Person.Create(Name, Age)

        End Function

    End Class

End Namespace

And now we can modify the markup slightly so that it successfully loads:

 

<People

  xmlns="https://blogs.msdn.com/mikehillberg/BuilderPattern"

  xmlns:builder="https://blogs.msdn.com/mikehillberg/BuilderPatternBuilder" >

 

  <builder:Person Name='Mike' Age='5' />

  <builder:Person Name='Dwayne' Age='7' />

 

</People>

 

To test it out, run this code:

 

Sub Main()

    Dim people As People

    people = XamlReader.Load(File.Open("..\..\Test.xaml", FileMode.Open))

    For Each person As Person In people

        Console.WriteLine("Name = " + person.Name + ", Age = " + person.Age.ToString())

    Next

End Sub

 

… and we get this output:

 

Name = Mike, Age = 5

Name = Dwayne, Age = 7

 

 

One last note about markup extensions … Notice that I named the builder class “PersonExtension”, but I referred to it from Xaml as “Person”. That’s possible because when Xaml doesn’t find “Person”, it automatically appends the “Extension” suffix, and looks for “PersonExtension”. If Person and PersonExtension had been in the same Clr namespace, that would have caused confusion, so I put them in different namespaces (“PeopleNamespace” and “PeopleBuilderNamespace”). Alternatively, I could have put them both in the “PeopleNamespace”, and used the specific <PersonExtension> tag (or maybe I would have named it “PersonBuilder”).