beechio
beechio

Reputation: 31

XAML serialization - specifying a property is required

I'm trying to use XAML to serialize/deserialize some custom (non-WPF/UI) information, and would like to enforce that certain properties are required.

XAML deserialization, by default, just creates each object with the default constructor, and then sets any properties it finds in the element's attributes, or in property element syntax. Any properties of the underlying object which are not specified in the XAML being serialized are just left as they are, i.e. whatever value they got after construction.

I'd like to know the best way of specifying that a certain property must be present in the XAML - and if not, have the deserialization fail.

I was expecting an attribute of some kind, but I can't find anything.

There are certain types in WPF which do exhibit this behaviour, but presumably WPF uses its own custom way of enforcing this. For example if you have..

<Setter Property="Height" ></Setter>

..the designer will complain 'The property "Value" is missing'.

I can think of some pretty convoluted ways of doing this:

  1. Have each property setter somehow record it was called, and then have custom code run after deserialization which checks that all 'required' properties were actually set.

  2. Use nullable properties everywhere, and then check after deserialization if any 'required' ones are still null. (Of course this won't work if null is a valid thing to set something to!)

  3. Perhaps there's a way of writing a custom XamlObjectWriter which can check for some [Required] attribute on the object's properties, and fail if these are not found by the XamlReader.

These all sound like a lot more work than I hoped - and I'm sure there's a more obvious way. Does anyone have any other ideas, or experience of (and maybe a solution to) this problem?

Upvotes: 3

Views: 1071

Answers (2)

McGuireV10
McGuireV10

Reputation: 9936

I know this is old, but I ran across the question while looking for a way to do this shortly before I figured it out. Maybe it'll help someone else. Fortunately this is easy: implement ISupportInitialize. I'm using WPF but it should work anywhere.

public class Hero : ISupportInitialize
{
    public string Who
    { get; set; } = string.Empty;

    public string What
    { get; set; } = string.Empty;

    public string Where
    { get; set; } = string.Empty;

    public void BeginInit()
    {
        // set a flag here if your property setters
        // have dependencies on other properties
    }

    public void EndInit()
    {
        if (string.IsNullOrWhiteSpace(Who))
            throw new Exception($"The property \"Who\" is missing");
        if (string.IsNullOrWhiteSpace(What))
            throw new Exception($"The property \"What\" is missing");
        // Where is optional...
    }
}

Who and What are required, but What is missing on the second entry:

<Window.Resources>
    <local:Hero x:Key="Badguy" Who="Vader" What="Sith" Where="Death Star"/>
    <local:Hero x:Key="Goodguy" Who="HanSolo" Where="Millenium Falcon"/>
</Window.Resources>

In the VS2017 XAML markup editor:

enter image description here

Upvotes: 2

fishgi
fishgi

Reputation: 31

I was facing a similar problem recently. After not being able to find any straightforward way, I decided to hook into the events of XamlObjectWriter to add custom support for this. It was basically what you suggested in point 3, except it turned out not really that complicated.

Basically it works like this: a dictionary is kept where each deserialized object is mapped to a set of remaining required properties. The BeforePropertiesHandler fills this set for the current object with all its properties with the RequiredAttribute. The XamlSetValueHandler removes the current property from the set. Finally, the AfterPropertiesHandler makes sure that there are no required properties left not set on the current object and throws an exception otherwise.

class RequiredAttribute : Attribute
{
}

public T Deserialize<T>(Stream stream)
{
    var requiredProperties = new Dictionary<object, HashSet<MemberInfo>>();

    var writerSettings = new XamlObjectWriterSettings
    {
        BeforePropertiesHandler = (sender, args) =>
        {
            var thisInstanceRequiredProperties = new HashSet<MemberInfo>();

            foreach(var propertyInfo in args.Instance.GetType().GetProperties())
            {
                if(propertyInfo.GetCustomAttribute<RequiredAttribute>() != null)
                {
                    thisInstanceRequiredProperties.Add(propertyInfo);
                }
            }

            requiredProperties[args.Instance] = thisInstanceRequiredProperties;
        },

        XamlSetValueHandler = (sender, args) =>
        {
            if(!requiredProperties.ContainsKey(sender))
            {
                return;
            }

            requiredProperties[sender].Remove(args.Member.UnderlyingMember);
        },

        AfterPropertiesHandler = (sender, args) =>
        {
            if(!requiredProperties.ContainsKey(args.Instance))
            {
                return;
            }

            var propertiesNotSet = requiredProperties[args.Instance];

            if(propertiesNotSet.Any())
            {
                throw new Exception("Required property \"" + propertiesNotSet.First().Name + "\" not set.");
            }

            requiredProperties.Remove(args.Instance);
        }
    };

    var readerSettings = new XamlXmlReaderSettings
    {
        LocalAssembly = Assembly.GetExecutingAssembly(),
        ProvideLineInfo = true
    };

    using(var reader = new XamlXmlReader(stream, readerSettings))
    using(var writer = new XamlObjectWriter(reader.SchemaContext, writerSettings))
    {
        XamlServices.Transform(reader, writer);
        return (T)writer.Result;
    }
}

Upvotes: 0

Related Questions