Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

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

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.)

like image 327
kasperhj Avatar asked Nov 12 '12 11:11

kasperhj


1 Answers

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

like image 67
khellang Avatar answered Sep 21 '22 23:09

khellang