在Caliburn.Micro中如何将动作绑定到嵌套的ViewModel方法?

4
我有一个使用Caliburn.Micro驱动的WPF应用程序,采用视图模型优先的方法。有一个命令栏类型的控件,使用CommandBarView.xaml和绑定的CommandBarViewModel。命令栏VM包含多个嵌套VM,每个按钮控件都有一个VM,它们展示了一个公共接口,并具有公共行为。命令栏VM将它们公开以便从视图中进行绑定。
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
}

这是一个关于CommandBarView的初步XAML示例:
<Button x:Name="UserNotFoundWarning_ShowWarning"
        IsEnabled="{Binding UserNotFoundWarning.CanShowWarning}">
  ... 
  <DataTrigger Binding="{Binding UserNotFoundWarning.IsVisible}" Value="True">
  ...
</Button>

以这种方式,我可以成功地绑定两个属性(CanShowWarning,IsVisible),但我无法将按钮命令/操作绑定到ShowWarning方法。
我尝试使用深度属性绑定,这对于属性再次起作用,但不适用于操作。 我还尝试了cal:Model.Bind和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>

似乎在运行时可以正常工作,但是cal:Model.Bind会使VS设计器完全无法使用,UI控件不会显示。
我已经搜索了很多,但我找不到一个真正的解决方案,让我也能够与设计器一起工作。对我来说很奇怪,我只能找到属性的深层绑定示例,而不是操作的深层绑定示例。
有什么想法如何解决这个问题吗?

你真的需要将UserNotFoundWarning作为“interface”属性吗?对于一个简单的按钮来说,使用VM似乎有些奇怪,一个简单的方法就可以了。 - H H
命令栏 VM 拥有许多这些按钮。每个按钮都订阅了一些业务级别的可观察对象,以检测它是否应该可见。因此,每个按钮都需要自己的 IsXxxWarningButtonVisible 逻辑,并且命令栏 VM 需要这些业务级别的依赖项,只是为了将它们传递给那个逻辑。然后,所有按钮共享与命令及其保护相关的公共逻辑,只有实际的警告文本在它们之间发生变化。我开始就是这样做的,但是后来我有很多 ShowXxxWarning、CanShowXxxWarning、IsXxxWarningVisible... 这些不太好。 - superjos
除了设计选择,我肯定可以重新考虑并讨论,一般来说有没有一种方法将事件/操作绑定到嵌套的VM方法? - superjos
1个回答

6

这是我的解决方案:

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();
}

在启动程序中调用EnableNestedViewModelActionBinding()一次,它将允许您使用通常的点符号将操作绑定到嵌套模型的方法。例如:

cal:Message.Attach="[Event Click] = [Action UserNotFoundWarning.ShowWarning]"

编辑:请注意,如果您在运行时更改嵌套的 ViewModel 实例,则此方法将不起作用。例如,如果在绑定发生后将 UserNotFoundWarning 分配给新的对象 - Caliburn 仍然会调用先前实例上的操作。


谢谢。我现在正在尝试您的解决方案,并阅读了一些限制。我们在运行时分配嵌套的ViewModels,在父级构造期间,但是它们在父级生命周期内永远不会改变,所以我想这应该可以工作。 - superjos
那个运行得很好,同时采用简化的符号 cal:Message.Attach="UserNotFoundWarning.ShowWarning"。谢谢! - superjos

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接