使用 IObservable 的对话交互请求

Dialog interaction requests using IObservable

我正在使用反应式编程来构建 MVVM 应用程序,并试图弄清楚我的视图模型如何提出问题并等待对话框提示用户回答。

例如,当用户单击“重命名”按钮时,我希望弹出一个允许用户更改文本的对话框。我的方法是让视图模型公开一个 IObservable<string> 属性。 View 中的代码隐藏侦听发出的值并可能显示 UWP ContentDialog。如果用户更改文本并单击“确定”,该对话框中的代码将在视图模型上调用 ReportResult(string newText)。我在下面有一些代码来展示它是如何工作的。两个问题:

这是从用户那里收集信息的合理方法吗?

此外,我有两种不同的构建方法,但不知道哪种更好。

interface IServiceRequest<TSource, TResult> : ISubject<TResult, TSource> { }

// Requests for information are just 'passed through' to listeners, if any.
class ServiceRequestA<TSource, TResult> : IServiceRequest<TSource, TResult>
{
    IObservable<TSource> _requests;
    Subject<TResult> _results = new Subject<TResult>();

    public ServiceRequestA(IObservable<TSource> requests)
    {
        _requests = requests;
    }

    public IObservable<TResult> Results => _results;
    public void OnCompleted() => _results.OnCompleted();
    public void OnError(Exception error) => _results.OnError(error);
    public void OnNext(TResult value) => _results.OnNext(value);
    public IDisposable Subscribe(IObserver<TSource> observer) => _requests.Subscribe(observer);
}

// Requests for information are 'parked' inside the class even if there are no listeners
// This happens when InitiateRequest is called. Alternately, this class could implement
// IObserver<TSource>.
class ServiceRequestB<TSource, TResult> : IServiceRequest<TSource, TResult>
{
    Subject<TSource> _requests = new Subject<TSource>();
    Subject<TResult> _results = new Subject<TResult>();

    public void InitiateRequest(TSource request) => _requests.OnNext(request);
    public IObservable<TResult> Results => _results;
    public void OnCompleted() => _results.OnCompleted();
    public void OnError(Exception error) => _results.OnError(error);
    public void OnNext(TResult value) => _results.OnNext(value);
    public IDisposable Subscribe(IObserver<TSource> observer) => _requests.Subscribe(observer);
}

class MyViewModel
{
    ServiceRequestA<string, int> _serviceA;
    ServiceRequestB<string, int> _serviceB;

    public MyViewModel()
    {
        IObservable<string> _words = new string[] { "apple", "banana" }.ToObservable();

        _serviceA = new ServiceRequestA<string, int>(_words);
        _serviceA
            .Results
            .Subscribe(i => Console.WriteLine($"The word is {i} characters long."));
        WordSizeServiceRequest = _serviceA;

        // Alternate approach using the other service implementation
        _serviceB = new ServiceRequestB<string, int>();
        IDisposable sub = _words.Subscribe(i => _serviceB.InitiateRequest(i)); // should dispose later
        _serviceB
            .Results
            .Subscribe(i => Console.WriteLine($"The word is {i} characters long."));
        WordSizeServiceRequest = _serviceB;
    }

    public IServiceRequest<string, int> WordSizeServiceRequest { get; set; }
    // Code outside the view model, probably in the View code-behind, would do this:
    // WordSizeServiceRequest.Select(w => w.Length).Subscribe(WordSizeServiceRequest);
}

根据 Lee Campbell 的评论,这里有一个不同的方法。也许他会更喜欢?我实际上不确定如何构建 IRenameDialog。之前它只是视图中的一些代码隐藏。

public interface IRenameDialog
{
    void StartRenameProcess(string original);
    IObservable<string> CommitResult { get; }
}

public class SomeViewModel
{
    ObservableCommand _rename = new ObservableCommand();
    BehaviorSubject<string> _name = new BehaviorSubject<string>("");

    public SomeViewModel(IRenameDialog renameDialog,string originalName)
    {
        _name.OnNext(originalName);
        _rename = new ObservableCommand();
        var whenClickRenameDisplayDialog =
            _rename
            .WithLatestFrom(_name, (_, n) => n)
            .Subscribe(n => renameDialog.StartRenameProcess(n));
        var whenRenameCompletesPrintIt =
            renameDialog
            .CommitResult
            .Subscribe(n =>
            {
                _name.OnNext(n);
                Console.WriteLine($"The new name is {n}");
            };
        var behaviors = new CompositeDisposable(whenClickRenameDisplayDialog, whenRenameCompletesPrintIt);
    }

    public ICommand RenameCommand => _rename;
}

嗯。 第一个代码块看起来像 IObservable<T> 的 re-implementation,实际上我认为情况更糟 ISubject<T>,所以这引起了警钟。

然后 MyViewModel class 做其他事情,比如将 IObservable<string> 作为参数传递(为什么?),在构造函数中创建订阅(副作用),并将服务公开为一个 public 属性。您还提到在您的视图代码后面有代码,这在 MVVM 中通常也是 code-smell。

我建议阅读 MVVM(已解决 10 年的问题)并了解其他客户端应用程序如何使用 Rx/Reactive MVVM 编程(已解决约 6 年的问题)

Lee 羞辱了我,让我想出了一个更好的解决方案。第一个也是最好的结果非常简单。我将其中之一传递给构造函数:

public interface IConfirmationDialog
{
    Task<bool> Show(string message);
}

在我的视图模型中,我可以做这样的事情...

IConfirmationDialog dialog = null; // provided by constructor
_deleteCommand.Subscribe(async _ =>
{
    var result = await dialog.Show("Want to delete?");
    if (result==true)
    {
        // delete the file
    }
});

构建一个 ConfirmationDialog 并不难。我只是在创建视图模型并将它们分配给视图的代码部分中创建其中之一。

public class ConfirmationDialogHandler : IConfirmationDialog
{
    public async Task<bool> Show(string message)
    {
        var dialog = new ConfirmationDialog(); // Is subclass of ContentDialog
        dialog.Message = message;
        var result = await dialog.ShowAsync();
        return (result == ContentDialogResult.Primary); 
    }
}

所以上面的解决方案很干净;我的视图模型需要的依赖项在构造函数中提供。另一种类似于 Prism 和 ReactiveUI 所做的方法是在没有所需依赖的情况下构建 ViewModel。相反,视图中有一些 code-behind 来填充该依赖项。我不需要多个处理程序,所以我只有这个:

public interface IInteractionHandler<TInput, TOutput>
{
    void SetHandler(Func<TInput, TOutput> handler);
    void RemoveHandler();
}

public class InteractionBroker<TInput, TOutput> : IInteractionHandler<TInput, TOutput>
{
    Func<TInput, TOutput> _handler;

    public TOutput GetResponse(TInput input)
    {
        if (_handler == null) throw new InvalidOperationException("No handler has been defined.");
        return _handler(input);
    }
    public void RemoveHandler() => _handler = null;

    public void SetHandler(Func<TInput, TOutput> handler) => _handler = handler ?? throw new ArgumentNullException();
}

然后我的 ViewModel 公开了一个 属性,如下所示:

public IInteractionHandler<string,Task<bool>> Delete { get; }

并像这样处理删除命令:

_deleteCommand.Subscribe(async _ =>
{
    bool shouldDelete  = await _deleteInteractionBroker.GetResponse("some file name");
    if (shouldDelete)
    {
        // delete the file
    }
});