问题
I have a WPF application, powered by Caliburn.Micro, view-model first approach. There's a command-bar type of control with its CommandBarView.xaml and bound CommandBarViewModel. Command-bar VM holds a number of nested VMs, one for each button control, all showing a common interface and having common behaviour. Command-bar VM expose them so they can be bound from view:
public interface IWarningButtonViewModel
{
bool IsVisible { get; }
bool CanShowWarning { get; }
void ShowWarning();
}
public class CommandBarViewModel : PropertyChangedBase
{
public IWarningButtonViewModel UserNotFoundWarning { get; private set; }
public IWarningButtonViewModel NetworkProblemWarning { get; private set; }
// ... initialization omitted for simplicity
}
This is a tentative XAML for a bit of CommandBarView:
<Button x:Name="UserNotFoundWarning_ShowWarning"
IsEnabled="{Binding UserNotFoundWarning.CanShowWarning}">
...
<DataTrigger Binding="{Binding UserNotFoundWarning.IsVisible}" Value="True">
...
</Button>
In this way I'm able to successfully bind the two properties (CanShowWarning, IsVisible) but I'm not able to bind the button command/action to ShowWarning method.
I tried with deep property binding and that works again for properties, but not for action.
I also tried with a mix of cal:Model.Bind
and cal:Message.Attach
:
<Button cal:Model.Bind="{Binding UserNotFoundWarning}"
cal:Message.Attach="[Event Click] = [Action ShowWarning]"
IsEnabled="{Binding CanShowWarning}">
...
<DataTrigger Binding="{Binding IsVisible}" Value="True">
...
</Button>
That seems to work at runtime, but cal:Model.Bind makes the VS designer completely unusable, UI controls are not shown.
I've searched around quite a bit, but I could not find an actual solution that let me also work with designer. It seems strange to me that I could only find examples of deep binding for properties, not for actions.
Any idea how to solve this?
回答1:
Here is my workaround:
private static void EnableNestedViewModelActionBinding()
{
var baseGetTargetMethod = ActionMessage.GetTargetMethod;
ActionMessage.GetTargetMethod = (message, target) =>
{
var methodName = GetRealMethodName(message.MethodName, ref target);
if (methodName == null)
return null;
var fakeMessage = new ActionMessage { MethodName = methodName };
foreach (var p in message.Parameters)
fakeMessage.Parameters.Add(p);
return baseGetTargetMethod(fakeMessage, target);
};
var baseSetMethodBinding = ActionMessage.SetMethodBinding;
ActionMessage.SetMethodBinding = context =>
{
baseSetMethodBinding(context);
var target = context.Target;
if (target != null)
{
GetRealMethodName(context.Message.MethodName, ref target);
context.Target = target;
}
};
}
private static string GetRealMethodName(string methodName, ref object target)
{
var parts = methodName.Split('.');
var model = target;
foreach (var propName in parts.Take(parts.Length - 1))
{
if (model == null)
return null;
var prop = model.GetType().GetPropertyCaseInsensitive(propName);
if (prop == null || !prop.CanRead)
return null;
model = prop.GetValue(model);
}
target = model;
return parts.Last();
}
Call EnableNestedViewModelActionBinding()
once from your bootstrapper and it will allow you to bind actions to nested model's methods using the usual dotted notation. E.g.
cal:Message.Attach="[Event Click] = [Action UserNotFoundWarning.ShowWarning]"
Edit: please note, that this wouldn't work if you change the nested ViewModel instance at runtime. E.g. if you assign your UserNotFoundWarning
to something new after the binding happened - Caliburn would still call actions on previous instance.
来源:https://stackoverflow.com/questions/31991203/in-caliburn-micro-how-to-bind-action-to-nested-viewmodel-method