#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 /// /// Binds a view to a view model. /// public static class ViewModelBinder { const string AsyncSuffix = "Async"; static readonly ILog Log = LogManager.GetLog(typeof(ViewModelBinder)); /// /// Gets or sets a value indicating whether to apply conventions by default. /// /// /// true if conventions should be applied by default; otherwise, false. /// public static bool ApplyConventionsByDefault = true; /// /// Indicates whether or not the conventions have already been applied to the view. /// public static readonly DependencyProperty ConventionsAppliedProperty = DependencyPropertyHelper.RegisterAttached( "ConventionsApplied", typeof(bool), typeof(ViewModelBinder), false ); /// /// Determines whether a view should have conventions applied to it. /// /// The view to check. /// Whether or not conventions should be applied to the view. public static bool ShouldApplyConventions(FrameworkElement view) { var overriden = View.GetApplyConventions(view); return overriden.GetValueOrDefault(ApplyConventionsByDefault); } /// /// Creates data bindings on the view's controls based on the provided properties. /// /// Parameters include named Elements to search through and the type of view model to determine conventions for. Returns unmatched elements. public static Func, Type, IEnumerable> BindProperties = (namedElements, viewModelType) => { var unmatchedElements = new List(); #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; }; /// /// Attaches instances of to the view's controls based on the provided methods. /// /// Parameters include the named elements to search through and the type of view model to determine conventions for. Returns unmatched elements. public static Func, Type, IEnumerable> 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); } /// /// Allows the developer to add custom handling of named elements which were not matched by any default conventions. /// public static Action, Type> HandleUnmatchedElements = (elements, viewModelType) => { }; /// /// Binds the specified viewModel to the view. /// ///Passes the the view model, view and creation context (or null for default) to use in applying binding. public static Action 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 namedElements = new List(); #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().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().ToList(); actionMessages.Apply(x => { x.applicationBarSource = menuItem; parsedTrigger.Actions.Remove(x); trigger.Actions.Add(x); }); triggers.Add(trigger); } } #endif } }