我正在使用反应式编程来构建MVVM应用程序,并试图弄清楚我的视图模型如何提出问题并等待对话框提示用户回答。
例如,当用户单击"重命名"按钮时,我希望弹出一个对话框,允许用户更改文本。我的方法是让视图模型公开一个IObservable<string>
属性。视图中的代码隐藏会侦听发出的值,并可能显示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>
的重新实现,实际上我认为事件更糟的是ISubject<T>
,所以这会敲响警钟。
然后MyViewModel
类执行其他操作,例如将IObservable<string>
作为参数传递(为什么?),在构造函数中创建订阅(副作用),并将服务公开为公共属性。您的视图中也有代码隐藏,这通常也是MVVM中的一种代码气味。
我建议阅读MVVM(解决问题10年),并了解其他客户端应用程序如何使用MVVM的Rx/Reactive编程(解决问题约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。相反,视图中有一些代码来填充该依赖关系。我不需要有多个处理程序,所以我只需要这个:
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
}
});