ReactiveCommand返回值和查看反馈循环
本文关键字:循环 返回值 ReactiveCommand | 更新日期: 2023-09-27 18:16:05
我熟悉MVVM的概念,并使用过MvvmCross,但我正在尝试ReactiveUI,并试图围绕一些概念来包装我的头。
我在WPF中编写一个工具(可能分支到其他框架),供设计人员创建和编辑数据文件,然后由另一个最终用户程序使用。我有一个代表DataModel
文档的ViewModel
,并希望对数据执行验证,以通知设计人员任何潜在的破坏行为。底层类如下所示:
public class DataModel
{
// member data here
public void Validate(Validator validator)
{
// perform specific complex validation here and add errors to validator
}
}
// aggregator for validation errors
public class Validator
{
public IList<Error> Errors { get; }
}
ViewModel
应该有View
可以绑定到按钮的ReactiveCommand Validate
,但是一旦完成,我想向用户显示一个对话框,显示验证错误或没有发现任何错误。是否有一种直接的方法可以将Validator.Errors
传递回View
,或者我必须为View
创建IObservable
或ReactiveList
属性来订阅?
开始编辑
感谢评论中的帮助,我可以弄清楚如何使用UserErrors来节省用户确认。仍在试图找出验证的返回值。以下是我到目前为止在ViewModel
中的内容:
public class ViewModel
{
public DataModel Model { get; }
public ReactiveCommand<List<Error>> { get; protected set; }
public ViewModel(DataModel model)
{
Model = model;
Validate = ReactiveCommand.CreateAsyncObservable<List<Error>>(_ =>
{
Validator validator = new Validator();
Model.Validate(validator);
// not sure how to turn validator.Errors into the IObservable<List<Error>> that ReactiveUI wants me to return.
});
}
}
把Validate
改成ReactiveCommand<Error>
,直接叫validator.Errors.ToObservable()
会更好吗?我还能遍历视图中的错误吗?
结束编辑
类似地,我希望有一个保存函数首先执行验证。如果没有发现验证错误,它将DataModel
保存到文件中。如果发现错误,View
应该通知用户并得到确认后再保存。ReactiveUI处理这个反馈循环的方法是:
执行Save
命令->验证(可能调用Validate
命令?)->如果错误,则从View
请求确认->保存确认或什么都不做
正如评论中提到的,这里有一些例子:https://github.com/reactiveui/ReactiveUI/blob/master/docs/basics/errors.md
:https://github.com/reactiveui/ReactiveUI.Samples/tree/master/ReactiveUI.Samples.Routing
关于不同错误类型的不同对话框,一种方法是您可以基于RecoveryCommand。例如,根据您提供的内容显示不同的选项,当您触发UserError时,您可以提供RecoveryCommands,然后根据它执行自定义逻辑。
然后在你处理错误的视图模型中,你可以这样做:
// The show command will return the decision from the user how to proceed with a error.
// The UserError will have a number of recovery options associated with it, which the dialog
// will present to the user. In testing mode this will likely be the test triggering the recovery command.
// We will wait until one of those recovery commands is executed, then get the result from it.
ShowCommand = ReactiveCommand.CreateAsyncObservable(x =>
{
var userError = x as UserError;
// We must always have a valid user error.
if (userError == null)
{
return Observable.Throw<RecoveryOptionResult>(new InvalidOperationException(string.Format(CultureInfo.InvariantCulture, "Expected a UserError but got {0}", x)));
}
Error = userError;
Message = Error.ErrorMessage;
// This fancy statement says go through all the recovery options we are presenting to the user
// subscribe to their is executing event, if the event fires, get the return result and pass that back
// as our return value for this command.
return (Error.RecoveryOptions.Select(cmd => cmd.IsExecuting.SelectMany(_ => !cmd.RecoveryResult.HasValue ? Observable.Empty<RecoveryOptionResult>() : Observable.Return(cmd.RecoveryResult.Value)))).Merge().Take(1);
});
我认为正如你所说的,只会执行一个命令。我基本上会将不同的恢复命令isexecution合并为一个,当第一个被点击时,我假设这是我想要的恢复选项。
当你抛出恢复命令时,你可以按你需要的方式处理它:
var retryCommand = new RecoveryCommand("Retry") { IsDefault = true };
retryCommand.Subscribe(_ => retryCommand.RecoveryResult = RecoveryOptionResult.RetryOperation);
var userError = new UserError(errorMessage, errorResolution, new[] { retryCommand, RecoveryCommand.Cancel });
switch (await UserError.Throw(userError))
{
case RecoveryOptionResult.RetryOperation:
await Setup();
break;
case RecoveryOptionResult.FailOperation:
case RecoveryOptionResult.CancelOperation:
if (HostScreen.Router.NavigateBack.CanExecute(null))
{
HostScreen.Router.NavigateBack.Execute(null);
};
break;
default:
throw new ArgumentOutOfRangeException();
}
另一种方法可能是派生UserError类,并根据出现的类类型显示不同的对话框。例如,根据类类型保留想要显示的控件的字典。当您为显示错误对话框注册处理程序时,只需显示适当的对话框。
下面的例子显示了使用命令进行验证。我使用了一个名为NavigationCommands.Search的内置命令。按钮:单击调用DataModel:Validate,如果发现错误,则填充传入的Validator。
MainWindow.xaml
<Window ...>
<Window.CommandBindings>
<CommandBinding Command="NavigationCommands.Search" CanExecute="CommandBinding_CanExecute" Executed="CommandBinding_Executed"/>
</Window.CommandBindings>
<Grid>
...
<Button Content="Button" Command="NavigationCommands.Search" HorizontalAlignment="Left" Margin="229,28,0,0" Grid.Row="1" VerticalAlignment="Top" Width="75"/>
</Grid>
</Window>
Mainwindow.xaml.cs
namespace WpfCommands
{
/// <summary>
/// Interaction logic for MainWindow.xaml
/// </summary>
public partial class MainWindow : Window
{
DataModel dm = new DataModel();
public MainWindow()
{
InitializeComponent();
}
private void CommandBinding_CanExecute(object sender, CanExecuteRoutedEventArgs e)
{
e.CanExecute = true;
}
private void CommandBinding_Executed(object sender, ExecutedRoutedEventArgs e)
{
Validator myValidator = new Validator();
dm.Validate(myValidator);
if (myValidator.Errors.Count > 0)
{
MessageBox.Show("Errors !");
// do something with errors
}
}
}
}
DataModel.cs
namespace WpfCommands
{
public class DataModel
{
public void Validate(Validator v)
{
// do some validation
v.Errors.Add(new Error() { Message = "Error1" });
}
}
// aggregator for validation errors
public class Validator
{
IList<Error> _errors = new List<Error>();
public IList<Error> Errors { get { return _errors; } }
}
public class Error
{
public string Message { get; set; }
}
}