Easing pass-through-properties in Caliburn.Micro similar to Catel's [ExposeAttribute]
Asked Answered
C

1

6

Does Caliburn.Micro have a similar function to Catel's [ExposeAttribute]?

Is there some other way to ease the work of pass-through-properties in Caliburn.Micro? (I.e. properties that are in the Model but also in the ViewModel to allow the View to access the properties.)

Coin answered 12/11, 2012 at 11:58 Comment(0)
A
7

Define the ExposeAttribute:

[AttributeUsage(AttributeTargets.Property, AllowMultiple = true)]
public class ExposeAttribute : Attribute
{
    public ExposeAttribute(string propertyName)
    {
        PropertyName = propertyName;
    }

    public ExposeAttribute(string propertyName, string modelPropertyName)
    {
        PropertyName = propertyName;
        ModelPropertyName = modelPropertyName;
    }

    public string PropertyName { get; set; }

    public string ModelPropertyName { get; set; }
}

And use this ExposedPropertyBinder I just wrote for you :)

public static class ExposedPropertyBinder
{
    private static readonly ILog Log = LogManager.GetLog(typeof(ExposedPropertyBinder));

    public static void BindElements(IEnumerable<FrameworkElement> elements, Type viewModelType)
    {
        foreach (var element in elements)
        {
            var parts = element.Name.Trim('_')
                .Split(new[] { '_' }, StringSplitOptions.RemoveEmptyEntries);

            // Get first exposed property
            var exposedPropertyInfo = GetExposedPropertyInfo(viewModelType, parts[0]);
            if (exposedPropertyInfo == null)
            {
                Log.Info("Binding Convention Not Applied: Element {0} did not match a property.", element.Name);
                continue;
            }

            var breadCrumb = new List<string> { exposedPropertyInfo.Path };

            // Loop over all parts and get exposed properties
            for (var i = 1; i < parts.Length; i++)
            {
                var exposedViewModelType = exposedPropertyInfo.ViewModelType;

                exposedPropertyInfo = GetExposedPropertyInfo(exposedViewModelType, parts[i]);
                if (exposedPropertyInfo == null) break;

                breadCrumb.Add(exposedPropertyInfo.Path);
            }

            if (exposedPropertyInfo == null)
            {
                Log.Info("Binding Convention Not Applied: Element {0} did not match a property.", element.Name);
                continue;
            }

            var convention = ConventionManager.GetElementConvention(element.GetType());
            if (convention == null)
            {
                Log.Warn("Binding Convention Not Applied: No conventions configured for {0}.", element.GetType());
                continue;
            }

            var applied = convention.ApplyBinding(exposedPropertyInfo.ViewModelType,
                string.Join(".", breadCrumb), exposedPropertyInfo.Property, element, convention);

            var appliedMessage = string.Format(applied 
                ? "Binding Convention Applied: Element {0}." 
                : "Binding Convention Not Applied: Element {0} has existing binding.", element.Name);

            Log.Info(appliedMessage);
        }
    }

    private static ExposedPropertyInfo GetExposedPropertyInfo(Type type, string propertyName)
    {
        foreach (var property in type.GetProperties())
        {
            if (property.Name.Equals(propertyName, StringComparison.OrdinalIgnoreCase))
                return new ExposedPropertyInfo(property.PropertyType, property.Name, property);

            // Get first ExposeAttribute which matches property name
            var exposeAttribute = GetExposeAttribute(property, propertyName);
            if (exposeAttribute == null) continue;

            // Get the name of the exposed property
            var exposedPropertyName = exposeAttribute.ModelPropertyName ?? exposeAttribute.PropertyName;

            var path = string.Join(".", property.Name, exposedPropertyName);
            var viewModelType = property.PropertyType;
            var propertyInfo = property;

            // Check if property exists
            var exposedProperty = viewModelType.GetPropertyCaseInsensitive(exposedPropertyName);
            if (exposedProperty == null)
            {
                // Do recursive check for exposed properties
                var child = GetExposedPropertyInfo(viewModelType, exposedPropertyName);
                if (child == null) continue;

                path = string.Join(".", property.Name, child.Path);
                viewModelType = child.ViewModelType;
                propertyInfo = child.Property;
            }

            return new ExposedPropertyInfo(viewModelType, path, propertyInfo);
        }

        return null;
    }

    private static ExposeAttribute GetExposeAttribute(PropertyInfo property, string propertyName)
    {
        return property
            .GetCustomAttributes(typeof(ExposeAttribute), true)
            .Cast<ExposeAttribute>()
            .FirstOrDefault(a => a.PropertyName.Equals(propertyName, StringComparison.OrdinalIgnoreCase));
    }

    private class ExposedPropertyInfo
    {
        public ExposedPropertyInfo(Type viewModelType, string path, PropertyInfo property)
        {
            ViewModelType = viewModelType;
            Path = path;
            Property = property;
        }

        public Type ViewModelType { get; private set; }

        public string Path { get; private set; }

        public PropertyInfo Property { get; private set; }
    }
}

Wire it up to Caliburn.Micro's ViewModelBinder like this:

ViewModelBinder.HandleUnmatchedElements = ExposedPropertyBinder.BindElements;

And voila!

Decorate your ViewModel properties with the ExposeAttribute:

public class MainViewModel : PropertyChangedBase
{
    private Person _person;

    [Expose("FirstName")]
    [Expose("LastName")]
    [Expose("ZipCode")]
    public Person Person
    {
        get { return _person; }
        set
        {
            _person = value;
            NotifyOfPropertyChange(() => Person);
        }
    }
}

public class Person
{
    public string FirstName { get; set; }

    public string LastName { get; set; }

    [Expose("ZipCode", "zip_code")]
    public Address Address { get; set; }

    public string FullName
    {
        get { return string.Join(" ", FirstName, LastName); }
    }

    public override string ToString()
    {
        return FullName;
    }
}

public class Address
{
    public string zip_code { get; set; }
}

And bind to your properties:

    <TextBlock x:Name="Person_FullName" />
    <TextBlock x:Name="FirstName" />
    <TextBlock x:Name="LastName" />
    <TextBlock x:Name="ZipCode" />                  //
    <TextBlock x:Name="Person_ZipCode" />           // THESE ARE THE SAME ;)

REMARK: This worked for my simple examples, but this has not been extensively tested so use it with care.

Hope it works for you! :)

EDIT: A slightly modified version can now be found on GitHub and NuGet

Appenzell answered 12/11, 2012 at 16:3 Comment(1)
That's pretty cool. Thanks! It will be some time before I get a good chance of testing this though.Coin

© 2022 - 2024 — McMap. All rights reserved.