Как передать Диспетчер пользовательского интерфейса в ViewModel
Я должен иметь возможность получить доступ к Dispatcher, который принадлежит представлению, мне нужно передать его ViewModel. Но View не должен ничего знать о ViewModel, так как вы его передаете? Ввести интерфейс или вместо того, чтобы передавать его экземплярам, создать глобальный диспетчер синглтона, который будет записан представлением? Как вы решаете это в ваших MVVM приложениях и фреймворках?
РЕДАКТИРОВАТЬ: Обратите внимание, что так как мои ViewModels могут быть созданы в фоновых потоках, я не могу просто сделать Dispatcher.Current
в конструкторе ViewModel.
15 ответов
Я абстрагировал Dispatcher с помощью интерфейса IContext:
public interface IContext
{
bool IsSynchronized { get; }
void Invoke(Action action);
void BeginInvoke(Action action);
}
Это дает преимущество в том, что вы можете проще тестировать свои модели ViewModels.
Я внедряю интерфейс в мои ViewModels, используя MEF (Managed Extensibility Framework). Другой возможностью будет аргумент конструктора. Однако мне больше нравится инъекция с использованием MEF.
Обновление (пример по ссылке в комментариях):
public sealed class WpfContext : IContext
{
private readonly Dispatcher _dispatcher;
public bool IsSynchronized
{
get
{
return this._dispatcher.Thread == Thread.CurrentThread;
}
}
public WpfContext() : this(Dispatcher.CurrentDispatcher)
{
}
public WpfContext(Dispatcher dispatcher)
{
Debug.Assert(dispatcher != null);
this._dispatcher = dispatcher;
}
public void Invoke(Action action)
{
Debug.Assert(action != null);
this._dispatcher.Invoke(action);
}
public void BeginInvoke(Action action)
{
Debug.Assert(action != null);
this._dispatcher.BeginInvoke(action);
}
}
Почему бы тебе не использовать
System.Windows.Application.Current.Dispatcher.Invoke(
(Action)(() => {ObservableCollectionMemeberOfVM.Add("xx"); } ));
вместо сохранения ссылки на диспетчер GUI.
Возможно, вам не нужен диспетчер. Если вы привязываете свойства вашей модели представления к элементам графического интерфейса в вашем представлении, механизм привязки WPF автоматически направляет обновления графического интерфейса в поток графического интерфейса с помощью диспетчера.
РЕДАКТИРОВАТЬ:
Это изменение является ответом на комментарий Исака Саво.
Внутри кода Microsoft для обработки привязки к свойствам вы найдете следующий код:
if (Dispatcher.Thread == Thread.CurrentThread)
{
PW.OnPropertyChangedAtLevel(level);
}
else
{
// otherwise invoke an operation to do the work on the right context
SetTransferIsPending(true);
Dispatcher.BeginInvoke(
DispatcherPriority.DataBind,
new DispatcherOperationCallback(ScheduleTransferOperation),
new object[]{o, propName});
}
Этот код выполняет маршализацию любых обновлений пользовательского интерфейса в потоке пользовательского интерфейса потока, так что даже если вы обновите свойства, принимающие участие в привязке из другого потока, WPF автоматически сериализует вызов потока пользовательского интерфейса.
Я получаю ViewModel для хранения текущего диспетчера в качестве члена.
Если ViewModel создается представлением, вы знаете, что текущий диспетчер во время создания будет диспетчером представления.
class MyViewModel
{
readonly Dispatcher _dispatcher;
public MyViewModel()
{
_dispatcher = Dispatcher.CurrentDispatcher;
}
}
Начиная с MVVM Light 5.2, библиотека теперь содержит DispatcherHelper
класс в GalaSoft.MvvmLight.Threading
пространство имен, которое предоставляет функцию CheckBeginInvokeOnUI()
который принимает делегата и запускает его в потоке пользовательского интерфейса. Это очень удобно, если в вашей ViewModel запущены некоторые рабочие потоки, которые влияют на свойства виртуальной машины, с которыми связаны ваши элементы пользовательского интерфейса.
DispatcherHelper
должен быть инициализирован с помощью вызова DispatcherHelper.Initialize()
на ранней стадии жизни вашего приложения (например, App_Startup
). Затем вы можете запустить любой делегат (или лямбду), используя следующий вызов:
DispatcherHelper.CheckBeginInvokeOnUI(
() =>
{
//Your code here
});
Обратите внимание, что класс определен в GalaSoft.MvvmLight.Platform
библиотека, на которую нет ссылки по умолчанию при добавлении через NuGet. Вы должны вручную добавить ссылку на эту библиотеку.
Другой распространенный шаблон (который сейчас широко используется в рамках) - это SynchronizationContext.
Это позволяет отправлять синхронно и асинхронно. Вы также можете установить текущий SynchronizationContext в текущем потоке, то есть его легко смоделировать. DispatcherSynchronizationContext используется приложениями WPF. Другие реализации SynchronizationContext используются WCF и WF4.
Начиная с версии 4.5 WPF можно использовать CurrentDispatcher
Dispatcher.CurrentDispatcher.Invoke(() =>
{
// Do GUI related operations here
}, DispatcherPriority.Normal);
Привет, может быть, я опоздал, так как прошло 8 месяцев с момента вашего первого поста... У меня была такая же проблема в приложении Silverlight Mvvm. и я нашел свое решение, как это. для каждой модели и модели представления у меня также есть класс с именем controller. как это
public class MainView : UserControl // (because it is a silverlight user controll)
public class MainViewModel
public class MainController
мой MainController отвечает за командование и связь между моделью и моделью представления. в конструкторе я создаю представление и его модель представления и устанавливаю текстовый вид представления для его модели представления.
mMainView = new MainView();
mMainViewModel = new MainViewModel();
mMainView.DataContext = mMainViewModel;
// (в моем соглашении об именах у меня есть префикс m для переменных-членов)
у меня также есть публичное свойство в типе моего MainView. как это
public MainView View { get { return mMainView; } }
(этот mMainView является локальной переменной для открытого свойства)
и теперь я готов. мне просто нужно использовать мой диспетчер для моего UI Therad, как это...
mMainView.Dispatcher.BeginInvoke(
() => MessageBox.Show(mSpWeb.CurrentUser.LoginName));
(в этом примере я просил мой контроллер получить мое имя пользователя sharepoint 2010, но вы можете делать то, что вам нужно)
мы почти закончили, вам также нужно определить свой корневой визуал в app.xaml следующим образом
var mainController = new MainController();
RootVisual = mainController.View;
это помогло мне моим заявлением. может быть, это может помочь вам тоже...
Для приложений магазина WPF и Windows использовать:-
System.Windows.Application.Current.Dispatcher.Invoke((Action)(() => {ObservableCollectionMemeberOfVM.Add("xx"); } ));
хранить ссылку на диспетчер GUI не совсем правильно.
если это не работает (например, в случае приложений для Windows Phone 8), используйте:-
Deployment.Current.Dispatcher
Вам не нужно передавать Диспетчер пользовательского интерфейса в ViewModel. Диспетчер пользовательского интерфейса доступен из единого приложения текущего приложения.
App.Current.MainWindow.Dispatcher
Это сделает вашу ViewModel зависимой от View. В зависимости от вашего приложения, это может или не может быть хорошо.
Если вам нужен только диспетчер для изменения связанной коллекции в другом потоке, взгляните на SynchronizationContextCollection здесь http://kentb.blogspot.com/2008/01/cross-thread-collection-binding-in-wpf.html
Работает хорошо, единственная проблема, которую я обнаружил, - это использование моделей представления со свойствами SynchronizationContextCollection с контекстом синхронизации ASP.NET, но их легко обойти.
HTH Сэм
Я нашел другой (самый простой) способ:
Добавьте для просмотра действия модели, которые следует вызывать в Dispatcher:
public class MyViewModel
{
public Action<Action> CallWithDispatcher;
public void SomeMultithreadMethod()
{
if(CallWithDispatcher != null)
CallWithDispatcher(() => DoSomethingMetod(SomeParameters));
}
}
И добавьте этот обработчик действия в конструктор представления:
public View()
{
var model = new MyViewModel();
DataContext = model;
InitializeComponent();
// Here
model.CallWithDispatcher += act => _taskbarIcon.Dispatcher
.BeginInvoke(DispatcherPriority.Normal, act) ;
}
Теперь у вас нет проблем с тестированием, и это легко реализовать. Я добавил это на свой сайт
Не нужно передавать диспетчер, если вы можете получить доступ к диспетчеру приложений, используя
Dispatcher dis = Application.Current.Dispatcher
Может быть, я немного опоздал к этому обсуждению, но я нашел 1 хорошую статью https://msdn.microsoft.com/en-us/magazine/dn605875.aspx
Есть 1 параграф
Кроме того, весь код вне слоя View (то есть слои ViewModel и Model, сервисы и т. Д.) Не должен зависеть от какого-либо типа, привязанного к конкретной платформе пользовательского интерфейса. Любое прямое использование Dispatcher (WPF/Xamarin/Windows Phone/Silverlight), CoreDispatcher (Windows Store) или ISynchronizeInvoke (Windows Forms) является плохой идеей. (SynchronizationContext немного лучше, но едва ли.) Например, в Интернете много кода, который выполняет некоторую асинхронную работу, а затем использует Dispatcher для обновления пользовательского интерфейса; более переносимым и менее громоздким решением является использование await для асинхронной работы и обновление пользовательского интерфейса без использования Dispatcher.
Предположим, что вы можете правильно использовать async / await, это не проблема.
С некоторыми из моих проектов WPF я столкнулся с такой же ситуацией. В моем MainViewModel (экземпляр Singleton) я получил статический метод CreateInstance(), принимающий диспетчер. И экземпляр create вызывается из View, чтобы я мог передать оттуда Dispatcher. И тестовый модуль ViewModel вызывает CreateInstance() без параметров.
Но в сложном многопоточном сценарии всегда полезно иметь реализацию интерфейса на стороне просмотра, чтобы получить правильный диспетчер текущего окна.