328 lines
13 KiB
C#
328 lines
13 KiB
C#
#if XFORMS
|
|
namespace Caliburn.Micro.Core.Xamarin.Forms
|
|
#else
|
|
namespace Caliburn.Micro
|
|
#endif
|
|
{
|
|
using System;
|
|
using System.Linq;
|
|
using System.Collections.Generic;
|
|
using System.Reflection;
|
|
using System.Threading.Tasks;
|
|
#if XFORMS
|
|
using UIElement = global::Xamarin.Forms.Element;
|
|
using FrameworkElement = global::Xamarin.Forms.VisualElement;
|
|
using DependencyProperty = global::Xamarin.Forms.BindableProperty;
|
|
using DependencyObject = global::Xamarin.Forms.BindableObject;
|
|
#elif WinRT81
|
|
using Windows.UI.Xaml;
|
|
using Microsoft.Xaml.Interactivity;
|
|
#else
|
|
using System.Windows;
|
|
using System.Windows.Interactivity;
|
|
using Caliburn.Micro.Core;
|
|
#endif
|
|
|
|
#if WINDOWS_PHONE
|
|
using Microsoft.Phone.Controls;
|
|
#endif
|
|
|
|
/// <summary>
|
|
/// Binds a view to a view model.
|
|
/// </summary>
|
|
public static class ViewModelBinder {
|
|
const string AsyncSuffix = "Async";
|
|
|
|
static readonly ILog Log = LogManager.GetLog(typeof(ViewModelBinder));
|
|
|
|
/// <summary>
|
|
/// Gets or sets a value indicating whether to apply conventions by default.
|
|
/// </summary>
|
|
/// <value>
|
|
/// <c>true</c> if conventions should be applied by default; otherwise, <c>false</c>.
|
|
/// </value>
|
|
public static bool ApplyConventionsByDefault = true;
|
|
|
|
/// <summary>
|
|
/// Indicates whether or not the conventions have already been applied to the view.
|
|
/// </summary>
|
|
public static readonly DependencyProperty ConventionsAppliedProperty =
|
|
DependencyPropertyHelper.RegisterAttached(
|
|
"ConventionsApplied",
|
|
typeof(bool),
|
|
typeof(ViewModelBinder),
|
|
false
|
|
);
|
|
|
|
|
|
/// <summary>
|
|
/// Determines whether a view should have conventions applied to it.
|
|
/// </summary>
|
|
/// <param name="view">The view to check.</param>
|
|
/// <returns>Whether or not conventions should be applied to the view.</returns>
|
|
public static bool ShouldApplyConventions(FrameworkElement view) {
|
|
var overriden = View.GetApplyConventions(view);
|
|
return overriden.GetValueOrDefault(ApplyConventionsByDefault);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Creates data bindings on the view's controls based on the provided properties.
|
|
/// </summary>
|
|
/// <remarks>Parameters include named Elements to search through and the type of view model to determine conventions for. Returns unmatched elements.</remarks>
|
|
public static Func<IEnumerable<FrameworkElement>, Type, IEnumerable<FrameworkElement>> BindProperties = (namedElements, viewModelType) => {
|
|
|
|
var unmatchedElements = new List<FrameworkElement>();
|
|
#if !XFORMS
|
|
foreach (var element in namedElements) {
|
|
var cleanName = element.Name.Trim('_');
|
|
var parts = cleanName.Split(new[] { '_' }, StringSplitOptions.RemoveEmptyEntries);
|
|
|
|
var property = viewModelType.GetPropertyCaseInsensitive(parts[0]);
|
|
var interpretedViewModelType = viewModelType;
|
|
|
|
for (int i = 1; i < parts.Length && property != null; i++) {
|
|
interpretedViewModelType = property.PropertyType;
|
|
property = interpretedViewModelType.GetPropertyCaseInsensitive(parts[i]);
|
|
}
|
|
|
|
if (property == null) {
|
|
unmatchedElements.Add(element);
|
|
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) {
|
|
unmatchedElements.Add(element);
|
|
Log.Warn("Binding Convention Not Applied: No conventions configured for {0}.", element.GetType());
|
|
continue;
|
|
}
|
|
|
|
var applied = convention.ApplyBinding(
|
|
interpretedViewModelType,
|
|
cleanName.Replace('_', '.'),
|
|
property,
|
|
element,
|
|
convention
|
|
);
|
|
|
|
if (applied) {
|
|
Log.Info("Binding Convention Applied: Element {0}.", element.Name);
|
|
}
|
|
else {
|
|
Log.Info("Binding Convention Not Applied: Element {0} has existing binding.", element.Name);
|
|
unmatchedElements.Add(element);
|
|
}
|
|
}
|
|
#endif
|
|
return unmatchedElements;
|
|
|
|
};
|
|
|
|
/// <summary>
|
|
/// Attaches instances of <see cref="ActionMessage"/> to the view's controls based on the provided methods.
|
|
/// </summary>
|
|
/// <remarks>Parameters include the named elements to search through and the type of view model to determine conventions for. Returns unmatched elements.</remarks>
|
|
public static Func<IEnumerable<FrameworkElement>, Type, IEnumerable<FrameworkElement>> BindActions = (namedElements, viewModelType) => {
|
|
var unmatchedElements = namedElements.ToList();
|
|
#if !XFORMS
|
|
#if WinRT || XFORMS
|
|
var methods = viewModelType.GetRuntimeMethods();
|
|
#else
|
|
var methods = viewModelType.GetMethods();
|
|
#endif
|
|
|
|
|
|
foreach (var method in methods) {
|
|
var foundControl = unmatchedElements.FindName(method.Name);
|
|
if (foundControl == null && IsAsyncMethod(method)) {
|
|
var methodNameWithoutAsyncSuffix = method.Name.Substring(0, method.Name.Length - AsyncSuffix.Length);
|
|
foundControl = unmatchedElements.FindName(methodNameWithoutAsyncSuffix);
|
|
}
|
|
|
|
if(foundControl == null) {
|
|
Log.Info("Action Convention Not Applied: No actionable element for {0}.", method.Name);
|
|
continue;
|
|
}
|
|
|
|
unmatchedElements.Remove(foundControl);
|
|
|
|
#if WinRT81
|
|
var triggers = Interaction.GetBehaviors(foundControl);
|
|
if (triggers != null && triggers.Count > 0)
|
|
{
|
|
Log.Info("Action Convention Not Applied: Interaction.Triggers already set on {0}.", foundControl.Name);
|
|
continue;
|
|
}
|
|
#endif
|
|
|
|
var message = method.Name;
|
|
var parameters = method.GetParameters();
|
|
|
|
if (parameters.Length > 0) {
|
|
message += "(";
|
|
|
|
foreach (var parameter in parameters) {
|
|
var paramName = parameter.Name;
|
|
var specialValue = "$" + paramName.ToLower();
|
|
|
|
if (MessageBinder.SpecialValues.ContainsKey(specialValue))
|
|
paramName = specialValue;
|
|
|
|
message += paramName + ",";
|
|
}
|
|
|
|
message = message.Remove(message.Length - 1, 1);
|
|
message += ")";
|
|
}
|
|
|
|
Log.Info("Action Convention Applied: Action {0} on element {1}.", method.Name, message);
|
|
Message.SetAttach(foundControl, message);
|
|
}
|
|
#endif
|
|
return unmatchedElements;
|
|
};
|
|
|
|
static bool IsAsyncMethod(MethodInfo method) {
|
|
return typeof(Task).IsAssignableFrom(method.ReturnType) &&
|
|
method.Name.EndsWith(AsyncSuffix, StringComparison.OrdinalIgnoreCase);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Allows the developer to add custom handling of named elements which were not matched by any default conventions.
|
|
/// </summary>
|
|
public static Action<IEnumerable<FrameworkElement>, Type> HandleUnmatchedElements = (elements, viewModelType) => { };
|
|
|
|
/// <summary>
|
|
/// Binds the specified viewModel to the view.
|
|
/// </summary>
|
|
///<remarks>Passes the the view model, view and creation context (or null for default) to use in applying binding.</remarks>
|
|
public static Action<object, DependencyObject, object> Bind = (viewModel, view, context) => {
|
|
#if !WinRT && !XFORMS
|
|
// when using d:DesignInstance, Blend tries to assign the DesignInstanceExtension class as the DataContext,
|
|
// so here we get the actual ViewModel which is in the Instance property of DesignInstanceExtension
|
|
if (View.InDesignMode) {
|
|
var vmType = viewModel.GetType();
|
|
if (vmType.FullName == "Microsoft.Expression.DesignModel.InstanceBuilders.DesignInstanceExtension") {
|
|
var propInfo = vmType.GetProperty("Instance", BindingFlags.Instance | BindingFlags.NonPublic);
|
|
viewModel = propInfo.GetValue(viewModel, null);
|
|
}
|
|
}
|
|
#endif
|
|
|
|
Log.Info("Binding {0} and {1}.", view, viewModel);
|
|
|
|
#if XFORMS
|
|
var noContext = Caliburn.Micro.Xamarin.Forms.Bind.NoContextProperty;
|
|
#else
|
|
var noContext = Caliburn.Micro.Bind.NoContextProperty;
|
|
#endif
|
|
|
|
if ((bool)view.GetValue(noContext)) {
|
|
Action.SetTargetWithoutContext(view, viewModel);
|
|
}
|
|
else {
|
|
Action.SetTarget(view, viewModel);
|
|
}
|
|
|
|
var viewAware = viewModel as IViewAware;
|
|
if (viewAware != null) {
|
|
Log.Info("Attaching {0} to {1}.", view, viewAware);
|
|
viewAware.AttachView(view, context);
|
|
}
|
|
|
|
if ((bool)view.GetValue(ConventionsAppliedProperty)) {
|
|
return;
|
|
}
|
|
|
|
var element = View.GetFirstNonGeneratedView(view) as FrameworkElement;
|
|
if (element == null) {
|
|
return;
|
|
}
|
|
|
|
#if WINDOWS_PHONE
|
|
BindAppBar(view);
|
|
#endif
|
|
|
|
if (!ShouldApplyConventions(element)) {
|
|
Log.Info("Skipping conventions for {0} and {1}.", element, viewModel);
|
|
#if WINDOWS_PHONE
|
|
view.SetValue(ConventionsAppliedProperty, true); // we always apply the AppBar conventions
|
|
#endif
|
|
return;
|
|
}
|
|
|
|
var viewModelType = viewModel.GetType();
|
|
#if SL5 || NET45
|
|
var viewModelTypeProvider = viewModel as ICustomTypeProvider;
|
|
if (viewModelTypeProvider != null) {
|
|
viewModelType = viewModelTypeProvider.GetCustomType();
|
|
}
|
|
#endif
|
|
#if XFORMS
|
|
IEnumerable<FrameworkElement> namedElements = new List<FrameworkElement>();
|
|
#else
|
|
var namedElements = BindingScope.GetNamedElements(element);
|
|
#endif
|
|
#if SILVERLIGHT
|
|
namedElements.Apply(x => x.SetValue(
|
|
View.IsLoadedProperty,
|
|
element.GetValue(View.IsLoadedProperty))
|
|
);
|
|
#endif
|
|
namedElements = BindActions(namedElements, viewModelType);
|
|
namedElements = BindProperties(namedElements, viewModelType);
|
|
HandleUnmatchedElements(namedElements, viewModelType);
|
|
|
|
view.SetValue(ConventionsAppliedProperty, true);
|
|
};
|
|
|
|
#if WINDOWS_PHONE
|
|
static void BindAppBar(DependencyObject view) {
|
|
var page = view as PhoneApplicationPage;
|
|
if (page == null || page.ApplicationBar == null) {
|
|
return;
|
|
}
|
|
|
|
var triggers = Interaction.GetTriggers(view);
|
|
|
|
foreach(var item in page.ApplicationBar.Buttons) {
|
|
var button = item as IAppBarActionMessage;
|
|
if (button == null || string.IsNullOrEmpty(button.Message)) {
|
|
continue;
|
|
}
|
|
|
|
var parsedTrigger = Parser.Parse(view, button.Message).First();
|
|
var trigger = new AppBarItemTrigger(button);
|
|
var actionMessages = parsedTrigger.Actions.OfType<ActionMessage>().ToList();
|
|
actionMessages.Apply(x => {
|
|
x.applicationBarSource = button;
|
|
parsedTrigger.Actions.Remove(x);
|
|
trigger.Actions.Add(x);
|
|
});
|
|
|
|
triggers.Add(trigger);
|
|
}
|
|
|
|
foreach (var item in page.ApplicationBar.MenuItems) {
|
|
var menuItem = item as IAppBarActionMessage;
|
|
if (menuItem == null || string.IsNullOrEmpty(menuItem.Message)) {
|
|
continue;
|
|
}
|
|
|
|
var parsedTrigger = Parser.Parse(view, menuItem.Message).First();
|
|
var trigger = new AppBarItemTrigger(menuItem);
|
|
var actionMessages = parsedTrigger.Actions.OfType<ActionMessage>().ToList();
|
|
actionMessages.Apply(x => {
|
|
x.applicationBarSource = menuItem;
|
|
parsedTrigger.Actions.Remove(x);
|
|
trigger.Actions.Add(x);
|
|
});
|
|
|
|
triggers.Add(trigger);
|
|
}
|
|
}
|
|
#endif
|
|
}
|
|
}
|