我试图遵循XamForms Playground中建立的视图模型导航模式,但遇到了一些麻烦,让它正常工作。
鉴于以下代码,您可以看到问题所在:
// Router = new RoutingState();
// Navigate = Router.NavigateCommandFor<MyViewModel>();
this.WhenAnyValue(x => x.ViewModel.Navigate)
//.Do(x => x.CanExecuteObservable.Subscribe())
.BindTo(this, x => x.NavigateButton.Command);
this.WhenAnyValue(x => x.ViewModel.Router)
.BindTo(this, x => x.ViewHost.Router);
如果我们只是将Navigate
命令绑定到按钮的Command
对象,则它的初始CanExecute
状态为false
,并且永远不会对该初始状态进行调整。如果我取消注释上面的Do
代码,那么我们强制要计算的初始状态,并且该按钮会自动启用。
显然,这对我来说看起来不正确,因为您可能期望初始CanExecute
状态为真(至少在加载UI之后)。
更糟糕的是,这似乎是Heisenbug,因为如果您检查CanExecute
或RoutingState.Navigate
命令的MyViewModel.Navigate
状态(使用{{ 1}}),它们将返回CanExecute(null)
并且按钮将在继续时显示启用(注意您只需检查视图模型命令以启用按钮)。
我想正确应用这种模式,但我很困惑为什么这种看似简单的模式在这种情况下失败了。任何人都可以确认或否认这是正确的方法吗?
答案 0 :(得分:0)
对这个问题做了一些更多的研究,我能够澄清一些困惑,但仍然没有完全回答这个问题。以下是我的发现:
首先,根据online documentation绑定命令的正确方法是
this.BindCommand(ViewModel, x => x.Navigate, x => x.NavigateButton);
我上面发布的代码示例应该是这样的,如果它在适当的(当前)ReactiveUI框架中:
this.BindCommand(ViewModel, x => x.Navigate, x => x.NavigateButton);
// note that I also used a "hack" binding for the router above, this is the correct binding strategy
this.OneWayBind(ViewModel, x => x.Router, x => x.ViewHost.Router);
为了(尽可能无缝地)为可路由视图模型创建导航命令,我在返回命令之前创建了一些执行CanExecuteObservable
订阅的扩展方法(暂时带有数字后缀)。在这个时候,我不确定扩展方法的最佳选择是什么,但是我倾向于数字3和4,因为它们产生一个强类型的通用命令(与{相比,它提供了自动可观察性) {1}})。
IReactiveCommand
我仍然不确定哪些扩展程序最合适,或者为什么订阅// all the NavigateCommandFor extensions also provide the ability to inject the navigate command itself (the default Navigate, or NavigateAndReset),
// the dependency resolver (still defaults to Locator.Current), and the type resolution contract
public static partial class ExtensionMethods
{
// simple extension to inline the subscription
public static T SubscribeToCommand<T>(this T This)
where T : IReactiveCommand
{
This.CanExecuteObservable.Subscribe();
return This;
}
// this was my first attempt without actually analyzing the current NavigateCommandFor source
public static IReactiveCommand<object> NavigateCommandFor1<T>(this RoutingState This, IReactiveCommand<object> navigationCommand = null, IDependencyResolver dependencyResolver = null, string contract = null)
where T : IRoutableViewModel
{
navigationCommand = navigationCommand ?? This.Navigate;
var ret = ReactiveCommand.CreateAsyncObservable(navigationCommand.CanExecuteObservable, _ => navigationCommand.ExecuteAsync((dependencyResolver ?? Locator.Current).GetService<T>(contract)));
return ret.SubscribeToCommand();
}
// After looking at the NavigateCommandFor source, this is minimalistic adaptation
// except ajusting the return value to be the generic interface with an object type param
public static IReactiveCommand<object> NavigateCommandFor2<T>(this RoutingState This, IReactiveCommand<object> navigationCommand = null, IDependencyResolver dependencyResolver = null, string contract = null)
where T : IRoutableViewModel
{
navigationCommand = navigationCommand ?? This.Navigate;
var ret = new ReactiveCommand<object>(navigationCommand.CanExecuteObservable, x => Observable.Return(x));
ret.Select(_ => (dependencyResolver ?? Locator.Current).GetService<T>(contract)).InvokeCommand(navigationCommand);
return ret.SubscribeToCommand();
}
// My attempt to optimize the return value to be strongly typed
// I'm not sure if this has any implications
public static IReactiveCommand<T> NavigateCommandFor3<T>(this RoutingState This, IReactiveCommand<object> navigationCommand = null, IDependencyResolver dependencyResolver = null, string contract = null)
where T : IRoutableViewModel
{
navigationCommand = navigationCommand ?? This.Navigate;
var ret = new ReactiveCommand<T>(navigationCommand.CanExecuteObservable, _ => Observable.Return((dependencyResolver ?? Locator.Current).GetService<T>(contract)));
ret.InvokeCommand(navigationCommand);
return ret.SubscribeToCommand();
}
// The original source allows for un-registered view models to be new()'d
// this extension provides that same ability, but again with a strongly typed return value
public static IReactiveCommand<T> NavigateCommandFor4<T>(this RoutingState This, IReactiveCommand<object> navigationCommand = null, IDependencyResolver dependencyResolver = null, string contract = null)
where T : IRoutableViewModel, new()
{
navigationCommand = navigationCommand ?? This.Navigate;
var ret = new ReactiveCommand<T>(navigationCommand.CanExecuteObservable, _ => Observable.Return((T)((IRoutableViewModel)(dependencyResolver ?? Locator.Current).GetService<T>(contract) ?? new T())));
ret.InvokeCommand(navigationCommand);
return ret.SubscribeToCommand();
}
}
是必要的(我认为这是CanExecuteObservable
中的错误),但上述情况可能如此被认为是我最初发布的问题的可行解决办法(但不是问题的答案,因为我仍然认为这不是ReactiveCommand
的正确的用法。)
答案 1 :(得分:0)
通过在构建反应命令时在observable上调用CanExecute
,我找到了一种更简洁的方法来播种Skip(1)
状态。这减少了扩展方法的一小部分代码,给出了以下内容:
// The key fix here is to Skip(1) on CanExecuteObservable so that we seed the state
// all the NavigateCommandFor extensions also provide the ability to inject the navigate command itself (the default Navigate, or NavigateAndReset),
// the dependency resolver (still defaults to Locator.Current), and the type resolution contract
public static partial class ExtensionMethods
{
// After looking at the NavigateCommandFor source, this is minimalistic adaptation
// except ajusting the return value to be the generic interface with an object type param
public static IReactiveCommand<object> PatchedNavigateCommandFor<T>(this RoutingState This, IReactiveCommand<object> navigationCommand = null, IDependencyResolver dependencyResolver = null, string contract = null)
where T : IRoutableViewModel
{
navigationCommand = navigationCommand ?? This.Navigate;
var ret = new ReactiveCommand<object>(navigationCommand.CanExecuteObservable.Skip(1), x => Observable.Return(x));
ret.Select(_ => (dependencyResolver ?? Locator.Current).GetService<T>(contract)).InvokeCommand(navigationCommand);
return ret;
}
// My attempt to optimize the return value to be strongly typed
// I'm not sure if this has any implications
public static IReactiveCommand<T> StrongNavigateCommandFor<T>(this RoutingState This, IReactiveCommand<object> navigationCommand = null, IDependencyResolver dependencyResolver = null, string contract = null)
where T : IRoutableViewModel
{
navigationCommand = navigationCommand ?? This.Navigate;
var ret = new ReactiveCommand<T>(navigationCommand.CanExecuteObservable.Skip(1), _ => Observable.Return((dependencyResolver ?? Locator.Current).GetService<T>(contract)));
ret.InvokeCommand(navigationCommand);
return ret;
}
// The original source allows for un-registered view models to be new()'d
// this extension provides that same ability, but again with a strongly typed return value
public static IReactiveCommand<T> StrongNewNavigateCommandFor<T>(this RoutingState This, IReactiveCommand<object> navigationCommand = null, IDependencyResolver dependencyResolver = null, string contract = null)
where T : IRoutableViewModel, new()
{
navigationCommand = navigationCommand ?? This.Navigate;
var ret = new ReactiveCommand<T>(navigationCommand.CanExecuteObservable.Skip(1), _ => Observable.Return((T)((IRoutableViewModel)(dependencyResolver ?? Locator.Current).GetService<T>(contract) ?? new T())));
ret.InvokeCommand(navigationCommand);
return ret;
}
}
我认为这是最恰当的答案,我还有posted an issue对ReactiveUI github项目的调查结果。