Связывание на основе состояния потока - PullRequest
0 голосов
/ 01 декабря 2011

У меня есть приложение WPF с несколькими десятками пользовательских панелей, отображаемых в списке, каждая панель представляет задачу для выполнения. На каждой панели будут кнопки «Пуск» и «Стоп» для управления потоком.

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

Итак, я хочу включить и отключить на основе 2 свойств: 1. Является ли переменная экземпляра частного фонового работника нулевой 2. Имеет ли публичный статический объект блокировку (полученную с помощью Monitor.Enter или lock)

Я хотел бы включить / отключить кнопки на основе следующей логики:

Кнопка «Пуск»: - включено, если открытый объект не заблокирован (означает, что потоки не запущены), иначе отключен (хотя бы один поток, возможно, из этого класса, работает)

Кнопка «Стоп» - Включено, если частный фоновый рабочий не равен NULL (поток из этого класса запускается / работает), иначе отключен (нет подходящих потоков для остановки)

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

Я довольно новичок в WPF и изучаю привязку данных. Я мог бы, вероятно, выяснить, как привязать к фоновому работнику == или! = Null, но я не уверен, как проверить, существует ли блокировка на объекте и как привязать к нему.

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

Создание пользовательской панели с двумя кнопками (привязки для кнопки остановки не реализованы)

<StackPanel Orientation="Horizontal">
<Button Margin="2" x:Name="btnStart" Content="Start" Click="btnStart_Click" IsEnabled="{Binding CanCommandsExecute}"/>
<Button Margin="2" x:Name="btnStop" Content="Stop"/>
</StackPanel>

Поместите несколько экземпляров этого в окно

<StackPanel Orientation="Vertical">
<wpfsample:TestControl/>
<wpfsample:TestControl/>
<wpfsample:TestControl/>
</StackPanel>

А вот код для TestControl

public partial class TestControl : UserControl, INotifyPropertyChanged
{
    private static bool IsLocked = false;
    private static object threadlock = new object();
    private BackgroundWorker _worker;

    public event PropertyChangedEventHandler PropertyChanged;

    private bool _canCommandsExecute = true;
    public bool CanCommandsExecute { 
        get { return _canCommandsExecute && (!IsLocked); } 
        set { _canCommandsExecute = value; OnPropertyChanged("CanCommandsExecute"); } }

    public TestControl()
    {
        DataContext = this;
        InitializeComponent();
    }

    private void btnStart_Click(object sender, RoutedEventArgs e)
    {
        Monitor.Enter(threadlock);
        try
        {
            IsLocked = true;
            this.CanCommandsExecute = false;
            _worker = new BackgroundWorker();
            _worker.DoWork += (x, y) => { Thread.Sleep(5000); };
            _worker.RunWorkerCompleted += WorkComplete;
            _worker.RunWorkerAsync();
        }
        catch { Monitor.Exit(threadlock); }
    }

    private void WorkComplete(object sender, EventArgs e)
    {
        IsLocked = false;
        this.CanCommandsExecute = true;
        Monitor.Exit(threadlock);
    }

    protected void OnPropertyChanged(string name)
    {
        PropertyChangedEventHandler handler = PropertyChanged;
        if (handler != null)
        {
            handler(this, new PropertyChangedEventArgs(name));
        }            
    }

}

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

Остается вопрос: как отключить ВСЕ кнопки запуска вместо одной? Я получаю блокировку статического объекта (который сейчас не работает, изучая это)

Надеюсь, этот пример поможет

Ответы [ 3 ]

2 голосов
/ 01 декабря 2011

Я бы, вероятно, связал свою кнопку с RelayCommand во ViewModel, который имеет CanExecute, связанный с флагом CanExecute.

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

Поскольку свойство Button Command будет привязано к RelayCommand, они будут автоматически включены / отключены, когда параметр CanExecute будет иметь значение false.

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

public class SomeBaseClass()
{
    Public SomeBaseClass(IEventAggregator eventAggregator)
    {
        eventAggregator.GetEvent<ThreadStartedEvent>().Subscribe(DisableCanExecute);
        eventAggregator.GetEvent<ThreadStoppedEvent>().Subscribe(EnableCanExecute);
    }

    private bool _canExecute;
    private ICommand _startCommand;
    private ICommand _endCommand;

    public ICommand StartCommand
    {
        get
        {
            if (_startCommand== null)
            {
                _startCommand= new RelayCommand(
                    param => StartThread(),
                    param => this.BackgroundWorker != null && this.CanExecute
                );
            }
            return _startCommand;
        }
    }

    public ICommand EndCommand
    {
        get
        {
            if (_endCommand== null)
            {
                _endCommand = new RelayCommand(
                    param => StopThread(),
                    param => this.IsRunning == true
                );
            }
            return _endCommand;
        }
    }

    public void DisableCanExecute(ThreadStartedEvent e)
    {
       CanExecute = false;
    }

    public void EnableCanExecute(ThreadStoppedEvent e)
    {
       CanExecute = true;
    }
}

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

Я также обычно использую MVVM Light версию RelayCommand, или вы можете сделать свой собственный.Я мог бы также использовать PRISM DelegateCommand, хотя он не перезапускается автоматически CanExecute() при изменении параметров.Базовое определение RelayCommand выглядит следующим образом:

/// <summary>
/// A command whose sole purpose is to relay its functionality to other
/// objects by invoking delegates. The default return value for the
/// CanExecute method is 'true'.
/// </summary>
public class RelayCommand : ICommand
{
    #region Fields

    readonly Action<object> _execute;
    readonly Predicate<object> _canExecute;

    #endregion // Fields

    #region Constructors

    /// <summary>
    /// Creates a new command that can always execute.
    /// </summary>
    /// <param name="execute">The execution logic.</param>
    public RelayCommand(Action<object> execute)
        : this(execute, null)
    {
    }

    /// <summary>
    /// Creates a new command.
    /// </summary>
    /// <param name="execute">The execution logic.</param>
    /// <param name="canExecute">The execution status logic.</param>
    public RelayCommand(Action<object> execute, Predicate<object> canExecute)
    {
        if (execute == null)
            throw new ArgumentNullException("execute");

        _execute = execute;
        _canExecute = canExecute;
    }

    #endregion // Constructors

    #region ICommand Members

    [DebuggerStepThrough]
    public bool CanExecute(object parameters)
    {
        return _canExecute == null ? true : _canExecute(parameters);
    }

    public event EventHandler CanExecuteChanged
    {
        add { CommandManager.RequerySuggested += value; }
        remove { CommandManager.RequerySuggested -= value; }
    }

    public void Execute(object parameters)
    {
        _execute(parameters);
    }

    #endregion // ICommand Members
}
1 голос
/ 05 декабря 2011

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

  1. Каждый экземпляр класса представлял собой пользовательскую панель, содержащую кнопки запуска и остановки
  2. В любой момент времени на экране может быть несколько экземпляров
  3. Когда кто-либо нажимал кнопку «Пуск», все остальные кнопки «Пуск» будут отключены до тех пор, пока задача не будет завершена.

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

@ Рейчел - Пока что это было немного не по себе, но я постараюсь найти решение, используя ваши предложения, когда узнаю больше о WPF.

Спасибо вам обоим за предложения.


В этом решении используется XAML из примера в моем вопросе (имя класса изменено на Testcase), но вся работа выполняется в коде позади. Там нет привязок данных.

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

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

public partial class Testcase : UserControl
{

    public static event EventHandler TestStarted;
    public static event EventHandler TestStopped;
    private static object lockobject = new object();

    private BackgroundWorker _worker;

    public Testcase()
    {
        InitializeComponent();

        //Register private event handlers with public static events
        Testcase.TestStarted += this.OnTestStart;
        Testcase.TestStopped += this.OnTestStop;

        //Set the default button states (start = enabled, stop = disabled)
        //Could be done in XAML, done here for clarity
        btnStart.IsEnabled = true;
        btnStop.IsEnabled = false;
    }

    private void OnTestStart(object sender, EventArgs e)
    {
        UpdateButtonStatus(sender, true);
    }

    private void OnTestStop(object sender, EventArgs e)
    {
        UpdateButtonStatus(sender, false);
    }

    private void UpdateButtonStatus(object eventCaller, bool testStarted)
    {
        Testcase testcase;
        if ((eventCaller is Testcase) && (eventCaller != null))
            testcase = (Testcase)eventCaller;
        else
            return;

        btnStart.IsEnabled = !testStarted;
        btnStop.IsEnabled = (eventCaller == this) && testStarted;
    }

    private void btnStart_Click(object sender, EventArgs e)
    {
        lock (Testcase.lockobject)
        {
            try
            {
                //Raise the event starting the test while still in the UI thread
                TestStarted(this, new EventArgs());

                //Use a background worker to execute the test in a second thread
                _worker = new BackgroundWorker() { WorkerReportsProgress = true, WorkerSupportsCancellation = true };                    
                _worker.DoWork += (x, y) => 
                    {
                        for (int i = 1; i <=50; i++)
                        {
                            if (_worker.CancellationPending)
                            {
                                y.Cancel = true;
                                break;
                            }
                            //Simulate work
                            Thread.Sleep(100); 
                        }                      
                    };
                _worker.RunWorkerCompleted += WorkComplete;
                _worker.RunWorkerAsync();
            }
            catch
            {
                //Ignore handling the error for the POC but raise the stopped event
                TestStopped(this, new EventArgs());
            }
        }
    }

    private void WorkComplete(object sender, EventArgs e)
    {
        TestStopped(this,new EventArgs());
    }

    private void btnStop_Click(object sender, EventArgs e)
    {
        //Terminate the background worker
        _worker.CancelAsync();
    }




}
1 голос
/ 02 декабря 2011

Не зная точно, какова ваша структура привязки (код, модели просмотра и т. Д.), Я бы посоветовал вам забыть о том, как заставить GUI / WPF понимать вашу базовую объектную модель и сосредоточиться на том, чтобы сделать ваш код простым для понимания. использовать из XAML.

Разумеется, не тратьте время на выяснение того, как связать XAML с тем, является ли что-то нулевым или нет, и заблокировано ли что-то еще. Вместо этого выставьте свойство из вашей цели привязки, которое разрешает их в соответствии с тем, что хочет объект.

Хорошей идеей является RelayCommand Рэйчел (или, возможно, DelegateCommand), так как это хороший способ работы с кнопками. Тем не менее, если вы новичок в WPF, это может быть немного для начала, с точки зрения реального понимания того, что происходит.

Предположим, что ваши кнопки привязываются к некоторому событию щелчка, которое обрабатывается в вашем коде:

public void ButtonClickHandler(/*Arguments elided*/)
{
    //Start the appropriate thread
}

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

public class MyPage : INotifyPropertyChanged
{

    private bool _canCommandsExecute;
    public bool CanCommandsExecute { get { return _canCommandsExecute; } set { _canCommandsExecute = value; RaisePropertyChanged("CanCommandsExecute"); } }

    public MyPage()
    {
         DataContext = this;
         InitializeComponent();
    }

    public void ButtonClickHandler(/*Arguments elided*/)
    {
         CanExecute = false;
         //Pseudocode: Thread.OnCompleted += (sender, completeargs) => CanExecute = true;
         //Start the appropriate thread
    }
}

Ваши кнопки в XAML затем привязываются к логическому свойству здесь для их свойств IsEnabled, которые будут установлены в false при запуске задачи, а затем обратно в true, когда задача завершится. Установщик свойств вызовет PropertyChanged для графического интерфейса, который обновит кнопки до включенных.

Чтобы было ясно, это концептуально легко понять, если вы новичок в фреймворке, но, на мой взгляд, не самый лучший способ сделать это. Это ступенька к лучшему способу сделать это. Как только вы поймете, что здесь происходит, вы можете изучить использование моделей представления для привязки и просмотреть кнопки привязки к RelayCommands или DelegateCommands на модели представления вместо использования обработчиков событий и кнопки IsEnabled. Так или иначе, это был мой опыт изучения WPF. ViewModels / Commands элегантны, но их преимущества легче понять, и почему они часто предпочтительнее, если вы сначала сделали это более простым для понимания способом с выделенным кодом.

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