Именно для этого были разработаны классы AutoResetEvent (и ManualResetEvent). Поэтому, за исключением небольшого загрязнения кода модели представления, я думаю, что это решение довольно опрятно.
из приятных особенностей MVVM является тестируемость ViewModel. В моем конкретном случае у меня есть виртуальная машина, которая загружает некоторые данные при вызове команды и соответствующий тест:
public class MyViewModel
{
public DelegateCommand LoadDataCommand { get; set; }
private List<Data> myData;
public List<Data> MyData
{
get { return myData; }
set { myData = value; RaisePropertyChanged(() => MyData); }
}
public MyViewModel()
{
LoadDataCommand = new DelegateCommand(OnLoadData);
}
private void OnLoadData()
{
// loads data over wcf or db or whatever. doesn't matter from where...
MyData = wcfClient.LoadData();
}
}
[TestMethod]
public void LoadDataTest()
{
var vm = new MyViewModel();
vm.LoadDataCommand.Execute();
Assert.IsNotNull(vm.MyData);
}
Так что это все довольно простые вещи. Тем не менее, что я действительно хотел бы сделать, это загрузить данные с помощьюBackgroundWorker
и на экране появится сообщение «загрузка». Поэтому я изменил ВМ на:
private void OnLoadData()
{
IsBusy = true; // view is bound to IsBusy to show 'loading' message.
var bg = new BackgroundWorker();
bg.DoWork += (sender, e) =>
{
MyData = wcfClient.LoadData();
};
bg.RunWorkerCompleted += (sender, e) =>
{
IsBusy = false;
};
bg.RunWorkerAsync();
}
Это прекрасно работает визуально во время выполнения, однако мой тест сейчас не проходит из-за того, что свойство не загружается сразу. Может кто-нибудь предложить хороший способ проверить этот вид загрузки? Я полагаю, что мне нужно что-то вроде:
[TestMethod]
public void LoadDataTest()
{
var vm = new MyViewModel();
vm.LoadDataCommand.Execute();
// wait a while and see if the data gets loaded.
for(int i = 0; i < 10; i++)
{
Thread.Sleep(100);
if(vm.MyData != null)
return; // success
}
Assert.Fail("Data not loaded in a reasonable time.");
}
Однако это кажется действительно неуклюжим ... Это работает, но просто чувствует себя грязным. Есть лучшие предложения?
Возможное решение:
Основываясь на ответе Дэвида Холла, чтобы издеваться над BackgroundWorker, я закончил делать эту довольно простую оберткуBackgroundWorker
это определяет два класса, один, который загружает данные асинхронно, и другой, который загружает синхронно.
public interface IWorker
{
void Run(DoWorkEventHandler doWork);
void Run(DoWorkEventHandler doWork, RunWorkerCompletedEventHandler onComplete);
}
public class AsyncWorker : IWorker
{
public void Run(DoWorkEventHandler doWork)
{
Run(doWork, null);
}
public void Run(DoWorkEventHandler doWork, RunWorkerCompletedEventHandler onComplete)
{
var bg = new BackgroundWorker();
bg.DoWork += doWork;
if(onComplete != null)
bg.RunWorkerCompleted += onComplete;
bg.RunWorkerAsync();
}
}
public class SyncWorker : IWorker
{
public void Run(DoWorkEventHandler doWork)
{
Run(doWork, null);
}
public void Run(DoWorkEventHandler doWork, RunWorkerCompletedEventHandler onComplete)
{
Exception error = null;
var args = new DoWorkEventArgs(null);
try
{
doWork(this, args);
}
catch (Exception ex)
{
error = ex;
throw;
}
finally
{
onComplete(this, new RunWorkerCompletedEventArgs(args.Result, error, args.Cancel));
}
}
}
Итак, в моей конфигурации Unity я могу использовать SyncWorker для тестирования и AsyncWorker для производства. Моя ViewModel становится:
public class MyViewModel(IWorker bgWorker)
{
public void OnLoadData()
{
IsBusy = true;
bgWorker.Run(
(sender, e) =>
{
MyData = wcfClient.LoadData();
},
(sender, e) =>
{
IsBusy = false;
});
}
}
Обратите внимание, что вещь, которую я отметил какwcfClient
на самом деле это тоже Mock в моих тестах, так что после звонкаvm.LoadDataCommand.Execute()
Я также могу подтвердить, чтоwcfClient.LoadData()
назывался.