Как выполнить обработку и сохранить графический интерфейс, обновленный с использованием привязки данных?

История проблемы

Это продолжение моего предыдущего вопроса

Как запустить поток для обновления GUI?

но поскольку Джон пролил новый свет на проблему, мне пришлось бы полностью переписать оригинальный вопрос, который сделает эту тему нечитаемой. Итак, новый, очень конкретный вопрос.

Проблема

Две части:

  • Процессорная голодная тяжелая обработка как библиотека (back-end)
  • WPF GUI с привязкой данных, которая служит в качестве монитора для обработки (front-end)

Текущая ситуация - библиотека отправляет так много уведомлений об изменениях данных, которые, несмотря на то, что она работает в пределах своего потока, полностью замалчивает механизм привязки данных WPF, и в результате не только мониторинг данных не работает (он не обновляется), но и весь Графический интерфейс заморожен во время обработки данных.

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

Пример

Это упрощенный пример, но он показывает проблему.

Часть XAML:

    <StackPanel Orientation="Vertical">
        <Button Click="Button_Click">Start</Button>
        <TextBlock Text="{Binding Path=Counter}"/>
    </StackPanel>

Часть С# (пожалуйста, обратите внимание, что это код одной части, но есть два его раздела):

public partial class MainWindow : Window,INotifyPropertyChanged
{
    // GUI part
    public MainWindow()
    {
        InitializeComponent();
        DataContext = this;
    }

    private void Button_Click(object sender, RoutedEventArgs e)
    {
        var thread = new Thread(doProcessing);
        thread.IsBackground = true;
        thread.Start();
    }

    // this is non-GUI part -- do not mess with GUI here
    public event PropertyChangedEventHandler PropertyChanged;

    public void OnPropertyChanged(string property_name)
    {
        if (PropertyChanged != null)
            PropertyChanged(this, new PropertyChangedEventArgs(property_name));
    }

    long counter;
    public long Counter
    {
        get { return counter; }
        set
        {
            if (counter != value)
            {
                counter = value;
                OnPropertyChanged("Counter");
            }
        }
    }


    void doProcessing()
    {
        var tmp = 10000.0;

        for (Counter = 0; Counter < 10000000; ++Counter)
        {
            if (Counter % 2 == 0)
                tmp = Math.Sqrt(tmp);
            else
                tmp = Math.Pow(tmp, 2.0);
        }
    }
}

Известные обходные пути

(Пожалуйста, не отправляйте их в качестве ответов)

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

  • Это мое, это уродливо, но простота его убивает - перед отправкой уведомления замораживать нить - Thread.Sleep(1) - чтобы потенциальный приемник "дышал" - он работает, он минималистичен, он уродлив, и он ВСЕГДА замедляет вычисление, даже если графический интерфейс отсутствует
  • на основе идеи Джона - отказаться от привязки данных ПОЛНОСТЬЮ (один виджет с привязкой данных достаточен для заклинивания), а вместо этого время от времени проверять данные и обновлять GUI вручную - Я не учил WPF просто отказаться от него сейчас; -)
  • Идея Thomas - вставьте прокси-сервер между библиотекой и интерфейсом, который будет получать все уведомления из библиотеки и передать некоторые из них в WPF, например, каждую секунду - недостатком является то, что вы должны дублировать все объекты, отправляющие уведомления
  • на основе идеи Jon - передать диспетчер графического интерфейса в библиотеку и использовать ее для отправки уведомлений - почему это уродливо? потому что вообще не может быть GUI

Мое текущее "решение" добавляет Sleep в основной цикл. Замедление незначительно, но достаточно, чтобы WPF обновлялся (так что даже лучше, чем спать перед каждым уведомлением).

Я все уши для реальных решений, а не некоторые трюки.

Примечания

Замечание об отказе от привязки данных - для меня дизайн его сломан, в WPF у вас есть единственный канал связи, вы не можете напрямую привязываться к источнику изменения. Связывание данных фильтрует источник на основе имени (строки!). Это требует некоторых вычислений, даже если вы используете какую-то умную структуру, чтобы сохранить все строки.

Изменить: Замечание об абстракциях - назовите мне старый таймер, но я начал изучать компьютер, убежденный, что компьютеры должны помогать людям. Повторяющиеся задачи - это область компьютеров, а не людей. Независимо от того, как вы это называете - MVVM, абстракции, интерфейс, одно наследование, если вы пишете один и тот же код, снова и снова, и у вас нет способа автоматизировать то, что вы делаете, вы используете сломанный инструмент. Так, например, lambdas отличные (меньше работы для меня), но одно наследование не является (больше работы для меня), привязка данных (как идея) велика (меньше работы), а необходимость прокси-уровня для КАЖДЫЙ библиотека, которую я связываю, - это сломанная идея, потому что она требует много работы.

Ответ 1

В моих приложениях WPF я не отправляю изменение свойств непосредственно из модели в графический интерфейс. Он всегда проходит через прокси (ViewModel).

События изменения свойств помещаются в очередь, которая считывается из потока GUI на таймере.

Не понимаю, как это может быть намного больше. Вам просто нужен еще один слушатель для вашего события changetime model.

Создайте класс ViewModel с свойством "Модель", которое является вашим текущим текстом данных. Измените привязку данных к "Model.Property" и добавьте код для подключения событий.

Это выглядит примерно так:

public MyModel Model { get; private set; }

public MyViewModel() {
    Model = new MyModel();
    Model.PropertyChanged += (s,e) => SomethingChangedInModel(e.PropertyName);
}

private HashSet<string> _propertyChanges = new HashSet<string>();

public void SomethingChangedInModel(string propertyName) {
    lock (_propertyChanges) {
        if (_propertyChanges.Count == 0)
            _timer.Start();
        _propertyChanges.Add(propertyName ?? "");
    }
}

// this is connected to the DispatherTimer
private void TimerCallback(object sender, EventArgs e) {
    List<string> changes = null;
    lock (_propertyChanges) {
        _Timer.Stop(); // doing this in callback is safe and disables timer
        if (!_propertyChanges.Contain(""))
            changes = new List<string>(_propertyChanges);
        _propertyChanges.Clear();
    }
    if (changes == null)
        OnPropertyChange(null);
    else
        foreach (string property in changes)
            OnPropertyChanged(property);
}

Ответ 2

Это не проблема WPF как таковая. Когда у вас многолетняя операция, которая быстро обновляет набор данных, обновление пользовательского интерфейса - любой пользовательский интерфейс, будь то WPF или WinForms или просто эмуляция VT100, будет представлять ту же проблему. Обновления пользовательского интерфейса сравнительно медленны и сложны, а их интеграция с быстро меняющимся сложным процессом без ущерба для этого процесса требует четкого разделения между ними.

Это чистое разделение еще важнее в WPF, потому что пользовательский интерфейс и длительная работа должны выполняться на отдельных потоках, чтобы пользовательский интерфейс не замерзал во время выполнения операции.

Как вы достигаете этого чистого разделения? Внедряя их самостоятельно, предоставляя механизм для периодического обновления пользовательского интерфейса из долговременного процесса, а затем проверяя все, чтобы выяснить, как часто этот механизм следует вызывать.

В WPF у вас будет три компонента: 1) представление, являющееся физической моделью вашего пользовательского интерфейса, 2) модель представления, которая является логической моделью данных, отображаемой в пользовательском интерфейсе, и что подталкивает изменения в данных к пользовательскому интерфейсу через уведомление об изменении, и 3) ваш длительный процесс.

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

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

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

После того, как вы построили эти три части, многопоточность очень проста для выполнения с использованием WPF BackgroundWorker. Вы создаете объект, который будет запускать процесс, передайте его отчет о проделанной работе с событием BackgroundWorker ReportProgress и маршалируйте данные из свойств объекта в модель представления в этом обработчике событий. Затем запустите метод long-running объекта в обработчике событий BackgroundWorker DoWork, и вы хорошо пойдете.

Ответ 3

Пользовательский интерфейс, который изменяется быстрее, чем человеческий глаз, может наблюдать (~ 25 обновлений/сек), не является удобным пользовательским интерфейсом. Типичный пользователь будет наблюдать зрелище не более минуты, прежде чем полностью отказаться. Вы прошли мимо этого, если вы сделали замораживание потока пользовательского интерфейса.

Вы должны проектировать для человека, а не для машины.

Ответ 4

Поскольку слишком много уведомлений для пользовательского интерфейса обрабатывается, почему бы не просто немного активировать уведомления? Кажется, это нормально работает:

        if (value % 500 == 0)
            OnPropertyChanged("Counter");

Вы также можете ограничить частоту уведомлений, используя таймер:

    public SO4522583()
    {
        InitializeComponent();
        _timer = new DispatcherTimer();
        _timer.Interval = TimeSpan.FromMilliseconds(50);
        _timer.Tick += new EventHandler(_timer_Tick);
        _timer.Start();
        DataContext = this;
    }

    private bool _notified = false;
    private DispatcherTimer _timer;
    void _timer_Tick(object sender, EventArgs e)
    {
        _notified = false;
    }

    ...

    long counter;
    public long Counter
    {
        get { return counter; }
        set
        {
            if (counter != value)
            {
                counter = value;
                if (!_notified)
                {
                    _notified = true;
                    OnPropertyChanged("Counter");
                }
            }
        }
    }

EDIT: если вы не можете пропускать уведомления, потому что они используются другими частями вашего кода, вот решение, которое не требует больших изменений в вашем коде:

  • создайте новое свойство UICounter, которое дросселирует уведомления, как показано выше.
  • в настройщике Counter, обновите UICounter
  • в пользовательском интерфейсе, привязитесь к UICounter, а не к Counter

Ответ 5

Требуется слой между UI и библиотекой. Это гарантирует, что вы сможете провести тестирование взаимодействия, а также позволить вам в будущем менять библиотеку с другой реализацией без особых изменений. Это не дублирование, а способ предоставления интерфейса для слоя UI для связи. Этот слой будет принимать объекты из библиотеки, преобразовывать их в конкретные объекты передачи данных и передавать их на другой уровень, который будет нести ответственность за дросселирование обновлений и преобразование их в конкретные объекты VM. Мое мнение таково, что виртуальные машины должны быть настолько тупыми, насколько это возможно, и их единственная ответственность должна заключаться в предоставлении данных для просмотра.

Ответ 6

Ваше звучание похоже на slow-down-refresh-rate-of-bound-datagrid. По крайней мере, ответы похожи

  • Имейте теневую копию данных, привязанных к элементу gui, вместо привязки исходных данных.
  • Добавьте обработчик событий, который обновляет теневую копию с определенной задержкой от исходных данных.

Ответ 7

Вам необходимо отключить источник уведомлений от цели для уведомлений. Теперь, как вы его настраиваете, каждый раз, когда значение изменяется, вы просматриваете весь цикл обновления (который, как я полагаю, также блокирует вашу функцию обработки). Это не то, что вы хотите.

Предоставить выходной поток для вашей функции обработки, которую он будет использовать для записи своих уведомлений.

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

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

Привет,

Родни