ReactiveCommand return 值和视图反馈循环
ReactiveCommand return values and View feedback loops
我熟悉 MVVM 的概念并使用过 MvvmCross,但我正在尝试 ReactiveUI 并试图围绕一些概念展开思考。
我正在 WPF 中编写一个工具(可能分支到其他框架)供设计人员创建和编辑数据文件,然后由另一个最终用户程序使用。我有一个 ViewModel
代表一个 DataModel
文档,我想对数据执行验证以通知设计人员任何潜在的破坏行为。基础 类 看起来像这样:
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
应该有一个 ReactiveCommand Validate
,View
可以绑定到一个按钮,但是一旦完成,我想向用户显示一个对话框,显示验证错误或者 none 被发现。有没有直接的方法将 Validator.Errors
传递回 View
,或者我必须为 [=17= 创建一个 IObservable
或 ReactiveList
属性 ] 订阅?
开始编辑
感谢评论中的帮助,我知道了如何使用 UserErrors 节省用户确认时间。仍在尝试找出 return 值以进行验证。这是我目前的 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
确认->确认时保存或什么都不做
以下示例显示了使用命令进行验证的用法。我使用了一个名为 NavigationCommands.Search 的内置命令。 Button:Click 调用 DataModel:Validate 如果发现错误,将填充传入的验证器。
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; }
}
}
正如评论中提到的,这里有一些例子:
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);
});
我假设你说的只会执行一个命令。我基本上会将不同的恢复命令 IsExecuting 组合成一个,当第一个被点击时,我假设这是我想要的恢复选项。
然后在你抛出恢复命令的地方你可以按你的需要处理它:
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 class,并根据 class 类型显示不同的对话框。例如,根据 class 类型保留要显示的控件字典。当您注册用于显示错误对话框的处理程序时,只需显示相应的对话框即可。
我熟悉 MVVM 的概念并使用过 MvvmCross,但我正在尝试 ReactiveUI 并试图围绕一些概念展开思考。
我正在 WPF 中编写一个工具(可能分支到其他框架)供设计人员创建和编辑数据文件,然后由另一个最终用户程序使用。我有一个 ViewModel
代表一个 DataModel
文档,我想对数据执行验证以通知设计人员任何潜在的破坏行为。基础 类 看起来像这样:
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
应该有一个 ReactiveCommand Validate
,View
可以绑定到一个按钮,但是一旦完成,我想向用户显示一个对话框,显示验证错误或者 none 被发现。有没有直接的方法将 Validator.Errors
传递回 View
,或者我必须为 [=17= 创建一个 IObservable
或 ReactiveList
属性 ] 订阅?
开始编辑
感谢评论中的帮助,我知道了如何使用 UserErrors 节省用户确认时间。仍在尝试找出 return 值以进行验证。这是我目前的 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
确认->确认时保存或什么都不做
以下示例显示了使用命令进行验证的用法。我使用了一个名为 NavigationCommands.Search 的内置命令。 Button:Click 调用 DataModel:Validate 如果发现错误,将填充传入的验证器。
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; }
}
}
正如评论中提到的,这里有一些例子: 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);
});
我假设你说的只会执行一个命令。我基本上会将不同的恢复命令 IsExecuting 组合成一个,当第一个被点击时,我假设这是我想要的恢复选项。
然后在你抛出恢复命令的地方你可以按你的需要处理它:
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 class,并根据 class 类型显示不同的对话框。例如,根据 class 类型保留要显示的控件字典。当您注册用于显示错误对话框的处理程序时,只需显示相应的对话框即可。