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