使用IObservable的对话交互请求

时间:2017-01-23 02:20:48

标签: system.reactive

我使用反应式编程来构建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。之前它只是View中的一些代码隐藏。

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;
}

2 个答案:

答案 0 :(得分:1)

嗯。 第一个代码块看起来像是IObservable<T>的重新实现,实际上我认为事件更糟ISubject<T>,因此会引发警钟。

然后MyViewModel类执行其他操作,例如传递IObservable<string>作为参数(为什么?),在构造函数中创建订阅(副作用),并将服务公开为公共属性。您还可以在后面的视图代码中使用代码,这通常也是MVVM中的代码味道。

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

答案 1 :(得分:1)

李让我感到羞耻,想出一个更好的解决方案。事实证明,第一个和最好的非常简单。我将其中一个传递给构造函数:

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而不需要依赖的方法。相反,在视图中有一些代码隐藏来填补该依赖关系。我不需要多个处理程序,所以我就是这样:

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; }

并像这样处理delete命令:

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