Мгновенное обновление связанного свойства WPF

У меня есть текстовое поле в окне, которое сообщает о статусе и/или успехе действия. Он привязан к свойству ViewModel.

Поэтому, когда пользователь активирует действие, которое может занять некоторое время, я не хочу сообщать ему, что действие началось в текстовом поле. Проблема в том, что он не пишет в текстовое поле, пока действие не завершится.

Я предполагаю, что это потому, что поток не дает пользовательскому интерфейсу возможности выполнять свою работу, но не уверен.

public class ViewModel : INotifyPropertyChanged
{
        private string _report;
        public string Report
        {
            get { return _report; }
            set
            {
                _report = value;
                RaisePropertyChanged("Report");
            }
        }

        public void DoHeavyAction()
        {
            Report += "Heavy action readying";

            ReadyHeavyAction();

            Report += "Heavy action starting";

            var result = DoTheHeavyAction();

            if(!result.success)
            {
                report += "Heavy action failed";
                return;
            }

            Report += result.value;
        }
}

В этом случае программа лагает на 2 секунды, а затем появляется все это:

Heavy action readying
Heavy action starting
Heavy action failed

Вместо того, чтобы появляться один за другим.

Могу ли я как-то использовать диспетчер для обновления представления (обратите внимание, я использую MVVM).


person Ingó Vals    schedule 15.11.2011    source источник
comment
Это не может быть правильным. Что такое отчет, _report и отчет?   -  person Ritch Melton    schedule 15.11.2011
comment
@RitchMelton Просто опечатка, underscore-report — это закрытое поле, доступ к которому осуществляется через свойство Report. отчет += должен быть отчет +=   -  person Ingó Vals    schedule 15.11.2011
comment
Предназначен ли также рекурсивный вызов DoHeavyAction()?   -  person Ritch Melton    schedule 15.11.2011
comment
@RitchMelton Нет, извините за это. Все должно было быть неважно, что такое тяжелое действие. Это просто для того, чтобы показать, что там происходит что-то сложное.   -  person Ingó Vals    schedule 15.11.2011


Ответы (3)


Да, это легко. Ваша тяжелая операция выполняется в отдельном потоке, и вы сообщаете о ходе выполнения как

Dispatcher.Invoke((Action)(() => {
    Report.value = "Started";
}));

Редактировать:
В данном случае операция активируется ICommand. Упомянутый ICommand отправляется из пользовательского интерфейса, поэтому он входит в поток пользовательского интерфейса. Итак, проблема в том, что пользовательский интерфейс (и его обновление) блокируется тяжелой операцией.

Решением было бы запустить новый поток по прибытии команды и выполнить работу там. Пользовательский интерфейс можно обновить с помощью кода, подобного приведенному выше примеру.

Есть несколько альтернативных подходов к этому решению, но все они так или иначе запускают задачу в фоновом потоке, хотя и неявно. Популярным и хорошим является использование BackgroundWorker и обновление состояние в событии ProgressChanged (обратите внимание, что это событие происходит в потоке пользовательского интерфейса, поэтому в этом случае вам не нужно использовать Dispatcher).

person Vlad    schedule 15.11.2011
comment
Где взять Диспетчера? - person Ingó Vals; 15.11.2011
comment
@MetalMikester Я думаю, что Dispatcher не является статическим, поэтому мне следует использовать тот, который поставляется с App.Xaml (здесь не уверен). Поскольку я использую MVVM, и все это происходит в ViewModel, у меня нет доступа к представлению. Я неправильно предполагаю? - person Ingó Vals; 15.11.2011
comment
Вы можете использовать this.Dispatcher или имя элемента управления.Dispatcher - person Tan; 15.11.2011
comment
В принципе, есть несколько способов. (1) в потоке пользовательского интерфейса вы получаете Dispatcher.CurrentDispatcher и сохраняете его в глобальной переменной. (2) вы запрашиваете диспетчера в любом из ваших элементов управления пользовательского интерфейса: button.Dispatcher. - person Vlad; 15.11.2011
comment
@Tan Используя MVVM, у меня нет доступа к элементу управления. Но я смог выполнить App.Current.Dispatcher, но это не сработало, как я надеялся. Кажется, что обновление поля не является проблемой. Вероятно, это больше связано с INotifyPropertyChanged, ожидающим уведомления или чего-то еще. - person Ingó Vals; 15.11.2011
comment
@Ingó Vals: О, тогда все по-другому. Я использую инфраструктуру Cinch v2 MVVM, в которой есть помощник диспетчера. - person MetalMikester; 16.11.2011
comment
@Ingó Vals: что именно пошло не так? Ваша модель представления обновляется в нужный момент? Или вид не отражает изменения в ВМ? - person Vlad; 16.11.2011
comment
@Vlad Да, вид действительно отражает изменения, просто не так, как я надеялся. Я хотел, чтобы информация появлялась. Поэтому перед началом тяжелой работы я хотел, чтобы появилось тяжелое действие, готовое, затем прошло 2-3 секунды, пока тяжелая работа готова, затем я хотел, чтобы началось появление тяжелого действия и т. д. Вместо этого проходит несколько секунд и тогда весь отчет (все, что я добавил в строку) появляется сразу. Кажется, что даже если я добавлю в свойство Report до того, как будет выполнена тяжелая работа, и будет вызвано событие propertyChanged, оно ожидает обновления пользовательского интерфейса. - person Ingó Vals; 16.11.2011
comment
@MetalMikester Я проверю Cinch, по крайней мере, для будущих проектов. Спасибо. - person Ingó Vals; 16.11.2011
comment
@Ingó Vals: что-то идет не так. Вы уверены, что не выполняете свою тяжелую работу в потоке пользовательского интерфейса? - person Vlad; 16.11.2011
comment
@Vlad Не уверен, я делаю все это в классе ViewModel, который связан с самим представлением, но установлен как его свойство DataContext. Однако он активируется ICommand из представления, так что это может быть. - person Ingó Vals; 16.11.2011
comment
@Ingó Vals: хорошо, тогда это ваша настоящая проблема :-) Команда определенно поступает в поток пользовательского интерфейса, поэтому вам нужно начать выполнять тяжелую работу в отдельном потоке. - person Vlad; 16.11.2011
comment
Спасибо, я использовал обычный класс Thread из .net, и он отлично работает. Не знаю, как мне дать правильный ответ, чтобы другим было понятно, кто может оказаться здесь. Не могли бы вы добавить к своему ответу предложение, в котором вы упоминаете, что потоки, запущенные из пользовательского интерфейса ICommand, выполняются в одном потоке (вероятно, общие сведения для многих, но не для всех). - person Ingó Vals; 16.11.2011

Вы можете переместить «тяжелую работу» в фоновую задачу, чтобы убрать ее из потока пользовательского интерфейса.

var task = Task.Factory.StartNew(() => DoHeavyAction());
Report += "Heavy Action Starting";
task.Wait();
person Ritch Melton    schedule 15.11.2011
comment
Нет, это опечатка, текст обновлен. Только не тогда, когда я тоже этого хочу. - person Ingó Vals; 15.11.2011
comment
Ваш ответ правильный. Я просто не видел этого, так как предполагал, что ViewModel каким-то образом будет работать в отдельном потоке, потому что он был связан с представлением (глупый я). Я использовал Thread вместо Task, но думаю, что результат тот же. Спасибо. - person Ingó Vals; 16.11.2011

Почему бы вам не использовать BackgroundWorker и не настроить ProgressChanged мероприятие?

Пример:

var worker = new BackgroundWorker();
worker.WorkerReportsProgress = true;
worker.ProgressChanged += WorkerReportProgress;
...

private static void WorkerReportProgress(object sender, ProgressChangedEventArgs e) {
    report += e.UserState.ToString();
}

и в DoWork сделать что-то вроде

private static void DoHeavyAction(object sender, DoWorkEventArgs e) {
    var worker = (BackgroundWorker)sender;
    worker.ReportProgress(0, "Heavy action readying");
    ReadyHeavyAction();
    ...
person dcarneiro    schedule 15.11.2011