Skip to content
Open
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
89 changes: 50 additions & 39 deletions src/Caliburn.Micro.Platform/ViewModelBinder.cs
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,8 @@
/// <summary>
/// Binds a view to a view model.
/// </summary>
public static class ViewModelBinder {
public static class ViewModelBinder
{
const string AsyncSuffix = "Async";

static readonly ILog Log = LogManager.GetLog(typeof(ViewModelBinder));
Expand Down Expand Up @@ -75,7 +76,8 @@
/// </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) {
public static bool ShouldApplyConventions(FrameworkElement view)
{
var overriden = View.GetApplyConventions(view);
return overriden.GetValueOrDefault(ApplyConventionsByDefault);
}
Expand All @@ -84,30 +86,35 @@
/// 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) => {
public static Func<IEnumerable<FrameworkElement>, Type, IEnumerable<FrameworkElement>> BindProperties = (namedElements, viewModelType) =>
{

var unmatchedElements = new List<FrameworkElement>();
#if !XFORMS && !MAUI
foreach (var element in namedElements) {
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++) {
for (int i = 1; i < parts.Length && property != null; i++)
{
interpretedViewModelType = property.PropertyType;
property = interpretedViewModelType.GetPropertyCaseInsensitive(parts[i]);
}

if (property == null) {
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) {
if (convention == null)
{
unmatchedElements.Add(element);
Log.Warn("Binding Convention Not Applied: No conventions configured for {0}.", element.GetType());
continue;
Expand All @@ -121,10 +128,12 @@
convention
);

if (applied) {
if (applied)
{
Log.Info("Binding Convention Applied: Element {0}.", element.Name);
}
else {
else
{
Log.Info("Binding Convention Not Applied: Element {0} has existing binding.", element.Name);
unmatchedElements.Add(element);
}
Expand All @@ -138,29 +147,33 @@
/// 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) => {
public static Func<IEnumerable<FrameworkElement>, Type, IEnumerable<FrameworkElement>> BindActions = (namedElements, viewModelType) =>
{
var unmatchedElements = namedElements.ToList();
#if !XFORMS && !MAUI
#if WINDOWS_UWP || XFORMS || MAUI
var methods = viewModelType.GetRuntimeMethods();
#else
var methods = viewModelType.GetMethods();
#endif


foreach (var method in methods) {
Log.Info($"Searching for methods control {method.Name} unmatchedElements count {unmatchedElements.Count}");

foreach (var method in methods)
{
Log.Info($"Searching for methods control {method.Name} unmatchedElements count {unmatchedElements.Count}");
var foundControl = unmatchedElements.FindName(method.Name);
if (foundControl == null && IsAsyncMethod(method)) {
if (foundControl == null && IsAsyncMethod(method))
{
var methodNameWithoutAsyncSuffix = method.Name.Substring(0, method.Name.Length - AsyncSuffix.Length);
foundControl = unmatchedElements.FindName(methodNameWithoutAsyncSuffix);
}

if(foundControl == null) {
if (foundControl == null)
{
Log.Info("Action Convention Not Applied: No actionable element for {0}. {1}", method.Name, unmatchedElements.Count);
foreach(var element in unmatchedElements)
foreach (var element in unmatchedElements)
{
Log.Info($"Unnamed element {element.Name}");
Log.Info($"Unnamed element {element.Name}");
}
continue;
}
Expand All @@ -179,18 +192,20 @@
var message = method.Name;
var parameters = method.GetParameters();

if (parameters.Length > 0) {
if (parameters.Length > 0)
{
message += "(";

foreach (var parameter in parameters) {
foreach (var parameter in parameters)
{
var paramName = parameter.Name;
var specialValue = "$" + paramName.ToLower();

if (MessageBinder.SpecialValues.ContainsKey(specialValue))
paramName = specialValue;

message += paramName + ",";
}

Check notice

Code scanning / CodeQL

Missed opportunity to use Select Note

This foreach loop immediately
maps its iteration variable to another variable
- consider mapping the sequence explicitly using '.Select(...)'.

message = message.Remove(message.Length - 1, 1);
message += ")";
Expand All @@ -203,7 +218,8 @@
return unmatchedElements;
};

static bool IsAsyncMethod(MethodInfo method) {
static bool IsAsyncMethod(MethodInfo method)
{
return typeof(Task).GetTypeInfo().IsAssignableFrom(method.ReturnType.GetTypeInfo()) &&
method.Name.EndsWith(AsyncSuffix, StringComparison.OrdinalIgnoreCase);
}
Expand All @@ -217,19 +233,8 @@
/// 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 !WINDOWS_UWP && !XFORMS && !MAUI
// 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

public static Action<object, DependencyObject, object> Bind = (viewModel, view, context) =>
{
Log.Info("Binding {0} and {1}.", view, viewModel);

#if XFORMS
Expand All @@ -240,29 +245,35 @@
var noContext = Caliburn.Micro.Bind.NoContextProperty;
#endif

if ((bool)view.GetValue(noContext)) {
if ((bool)view.GetValue(noContext))
{
Action.SetTargetWithoutContext(view, viewModel);
}
else {
else
{
Action.SetTarget(view, viewModel);
}

var viewAware = viewModel as IViewAware;
if (viewAware != null) {
if (viewAware != null)
{
Log.Info("Attaching {0} to {1}.", view, viewAware);
viewAware.AttachView(view, context);
}

if ((bool)view.GetValue(ConventionsAppliedProperty)) {
if ((bool)view.GetValue(ConventionsAppliedProperty))
{
return;
}

var element = View.GetFirstNonGeneratedView(view) as FrameworkElement;
if (element == null) {
if (element == null)
{
return;
}

if (!ShouldApplyConventions(element)) {
if (!ShouldApplyConventions(element))
{
Log.Info("Skipping conventions for {0} and {1}.", element, viewModel);
return;
}
Expand Down
Loading