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创建IObservableReactiveList属性来订阅?

开始编辑

感谢评论中的帮助,我可以弄清楚如何使用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请求确认->保存确认或什么都不做

ReactiveCommand返回值和查看反馈循环

正如评论中提到的,这里有一些例子: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; }
    }
}