Как выполнять обработку и обновлять графический интерфейс, используя привязку данных? - PullRequest
14 голосов
/ 24 декабря 2010

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

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

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

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

Задача

Две части:

  • Обработка большого количества ресурсов ЦП в виде библиотеки (серверная часть)
  • WPF GUI с привязкой данных, которая служит монитором для обработки (интерфейс)

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

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

Пример

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

Часть XAML:

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

Часть C # (пожалуйста, ОБРАТИТЕ ВНИМАНИЕ, это цельный код, но есть две его части):

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);
        }
    }
}

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

(Пожалуйста, не публикуйте их как ответы)

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

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

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

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

Замечания

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

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

Ответы [ 7 ]

5 голосов
/ 25 декабря 2010

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

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

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

Создайте класс ViewModel со свойством «Model», которое является вашим текущим текстом данных. Измените привязки данных на «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);
}
4 голосов
/ 24 декабря 2010

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

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

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

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

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

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

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

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

3 голосов
/ 24 декабря 2010

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

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

1 голос
/ 24 декабря 2010

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

        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");
                }
            }
        }
    }

РЕДАКТИРОВАТЬ: если вы не можете позволить себе пропускать уведомления, потому что они используются другими частями вашего кода, вот решение, которое не требует больших изменений в вашем коде:

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

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

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

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

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

С уважением,

Родни

0 голосов
/ 25 декабря 2010

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

  • Имейте теневую копию ваших данных, привязанную к элементу графического интерфейса вместо связывания исходных данных.
  • Добавить обработчик событий, который обновляет теневую копию с определенной задержкой по сравнению с исходными данными.
0 голосов
/ 25 декабря 2010

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

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...