Skip to content
Draft
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
98 changes: 64 additions & 34 deletions src/Caliburn.Micro.Platform/ViewModelBinder.cs
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
using System.Collections.Generic;
using System.Reflection;
using System.Threading.Tasks;
using System.Text;
#if XFORMS
using UIElement = global::Xamarin.Forms.Element;
using FrameworkElement = global::Xamarin.Forms.VisualElement;
Expand Down Expand Up @@ -41,7 +42,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 +77,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 +87,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 +129,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 +148,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 @@ -176,25 +190,31 @@
}
#endif

var message = method.Name;
var parameters = method.GetParameters();
var messageBuilder = new StringBuilder(method.Name);

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

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 + ",";
messageBuilder.Append(paramName).Append(",");
}

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 += ")";
// Remove the trailing comma
if (parameters.Length > 0)
messageBuilder.Length -= 1;
Comment on lines +212 to +213
Copy link

Copilot AI Aug 9, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This condition check is redundant since this code is already inside a block that only executes when parameters.Length > 0 (line 183). The condition will always be true and can be removed.

Suggested change
if (parameters.Length > 0)
messageBuilder.Length -= 1;
messageBuilder.Length -= 1;

Copilot uses AI. Check for mistakes.
messageBuilder.Append(")");

}
var message = messageBuilder.ToString();

Log.Info("Action Convention Applied: Action {0} on element {1}.", method.Name, message);
Message.SetAttach(foundControl, message);
Expand All @@ -203,7 +223,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,13 +238,16 @@
/// 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) => {
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) {
if (View.InDesignMode)
{
var vmType = viewModel.GetType();
if (vmType.FullName == "Microsoft.Expression.DesignModel.InstanceBuilders.DesignInstanceExtension") {
if (vmType.FullName == "Microsoft.Expression.DesignModel.InstanceBuilders.DesignInstanceExtension")

Check warning

Code scanning / CodeQL

Erroneous class compare Warning

Erroneous class compare.

Copilot Autofix

AI 3 months ago

To fix the problem, replace the string-based type comparison (vmType.FullName == "Microsoft.Expression.DesignModel.InstanceBuilders.DesignInstanceExtension") with a direct type comparison using == typeof(...). This ensures that the check is robust and not vulnerable to spoofing by types with the same name in different assemblies. The change should be made only in the relevant region (line 249) of src/Caliburn.Micro.Platform/ViewModelBinder.cs. No new imports are needed, as typeof is a built-in C# operator.


Suggested changeset 1
src/Caliburn.Micro.Platform/ViewModelBinder.cs

Autofix patch

Autofix patch
Run the following command in your local git repository to apply this patch
cat << 'EOF' | git apply
diff --git a/src/Caliburn.Micro.Platform/ViewModelBinder.cs b/src/Caliburn.Micro.Platform/ViewModelBinder.cs
--- a/src/Caliburn.Micro.Platform/ViewModelBinder.cs
+++ b/src/Caliburn.Micro.Platform/ViewModelBinder.cs
@@ -246,7 +246,7 @@
             if (View.InDesignMode)
             {
                 var vmType = viewModel.GetType();
-                if (vmType.FullName == "Microsoft.Expression.DesignModel.InstanceBuilders.DesignInstanceExtension")
+                if (vmType == Type.GetType("Microsoft.Expression.DesignModel.InstanceBuilders.DesignInstanceExtension", false))
                 {
                     var propInfo = vmType.GetProperty("Instance", BindingFlags.Instance | BindingFlags.NonPublic);
                     viewModel = propInfo.GetValue(viewModel, null);
EOF
@@ -246,7 +246,7 @@
if (View.InDesignMode)
{
var vmType = viewModel.GetType();
if (vmType.FullName == "Microsoft.Expression.DesignModel.InstanceBuilders.DesignInstanceExtension")
if (vmType == Type.GetType("Microsoft.Expression.DesignModel.InstanceBuilders.DesignInstanceExtension", false))
{
var propInfo = vmType.GetProperty("Instance", BindingFlags.Instance | BindingFlags.NonPublic);
viewModel = propInfo.GetValue(viewModel, null);
Copilot is powered by AI and may make mistakes. Always verify output.
Unable to commit as this autofix suggestion is now outdated
{
var propInfo = vmType.GetProperty("Instance", BindingFlags.Instance | BindingFlags.NonPublic);
viewModel = propInfo.GetValue(viewModel, null);
}
Expand All @@ -240,29 +264,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