Использование секундомера и привязки данных в окне WPF, которое уже обновляется с помощью IProgress

В моей WPF-программе Main() я запускаю трудоемкий метод асинхронно. Когда этот метод запущен, я запускаю дополнительное окно, содержащее ProgressBar, которое я обновляю с помощью IProgress.

Ниже приведен пример моей установки.

ОСНОВНАЯ программа:

public partial class MainWindow : Window
{
    private ProgressBarWindow pbwWindow = null;

    public MainWindow()
    {
        InitializeComponent();
    }

    private void RunMethodAsync(IProgress<int> progress)
    {
        Dispatcher.Invoke(() =>
        {
            pbwWindow = new ProgressBarWindow("Processing...");
            pbwWindow.Owner = this;
            pbwWindow.Show();
        });

        TimeConsumingMethod(progress);
    }

    private void TimeConsumingMethod(IProgress<int> progress)
    {
        for (int i = 1; i <= 100; i++)
        {
            // Thread.Sleep() represents actual time consuming work being done.
            Thread.Sleep(100);
            progress.Report(i);
        }
    }

    private async void btnRun_Click(object sender, RoutedEventArgs e)
    {
        IProgress<int> progress;

        progress = new Progress<int>(i => pbwWindow.SetProgressUpdate(i));
        await Task.Run(() => RunMethodAsync(progress));
    }
}

Мой ProgressBarWindow, который содержит индикатор выполнения, выглядит так:

public partial class ProgressBarWindow : Window
{
    Stopwatch stopwatch = new Stopwatch();
    BackgroundWorker worker = new BackgroundWorker();
    public string ElapsedTimeString { get; set; }

    public ProgressBarWindow(string infoText)
    {
        InitializeComponent();
        SetTimer();
    }

    private void Window_Loaded(object sender, RoutedEventArgs e)
    {
        StartTimer();
    }

    private void SetTimer()
    {
        worker.WorkerReportsProgress = true;
        worker.WorkerSupportsCancellation = true;

        worker.DoWork += (s, e) =>
        {
            while (!worker.CancellationPending)
            {
                worker.ReportProgress(0, stopwatch.Elapsed);
                Thread.Sleep(1000);
            }
        };

        worker.ProgressChanged += (s, e) =>
        {
            TimeSpan elapsedTime = (TimeSpan)e.UserState;
            ElapsedTimeString = string.Format("{0}:{1}:{2}", elapsedTime.Minutes, elapsedTime.Seconds, elapsedTime.Milliseconds);
        };
    }

    private void StartTimer()
    {
        stopwatch.Start();
        worker.RunWorkerAsync();
    }

    private void StopTimer()
    {
        stopwatch.Stop();
        worker.CancelAsync();
    }

    public void SetProgressUpdate(int progress)
    {
        pbLoad.Value = progress;
        if (progress >= 100)
        {
            StopTimer();
            Close();
        }
    }
}

Я позаимствовал логику StopWatch из этого ответа SO. Затем в моем ProgressBarWindow у меня есть TextBlock, который я использовал Binding следующим образом, как сказано в ответе выше.

<TextBlock Name="tbElapsedTime" Text="{Binding ElapsedTimeString}"/>

Теперь, когда я запускаю программу, метод выполняется, и индикатор выполнения обновляется просто отлично. Однако мой TextBlock, который должен обновляться с истекшим временем, не обновляется.

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

worker.ProgressChanged += (s, e) =>
        {
            TimeSpan elapsedTime = (TimeSpan)e.UserState;
            ElapsedTimeString = string.Format("{0}:{1}:{2}", elapsedTime.Minutes, elapsedTime.Seconds, elapsedTime.Milliseconds);
            tbElapsedTime.Text = ElapsedTimeString;
        };

Итак, я предполагаю, что моя проблема связана с привязкой и, возможно, использованием BackgroundWorker в окнах, которые уже запускаются асинхронно? Как я могу это исправить, чтобы использовать DataBinding?


person Sach    schedule 28.07.2017    source источник
comment
если я что-то не упустил, вы, похоже, не реализуете INotifyPropertyChanged на ElapsedTimeString. Значение изменится, но ваша привязка не будет обновляться, если вы не уведомите слой пользовательского интерфейса.   -  person Ginger Ninja    schedule 28.07.2017


Ответы (1)


Как упоминал Ginger Ninja, вы должны реализовать INotifyPropertyChanged и использовать RelativeSource={RelativeSource Self} (как дополнительную настройку привязки):

public partial class MainWindow : Window, INotifyPropertyChanged
{
    public event PropertyChangedEventHandler PropertyChanged;

    private string _ElapsedTimeString;
    public string ElapsedTimeString
    {
        get { return _ElapsedTimeString; }
        set
        {
            if (_ElapsedTimeString != value)
            {
                _ElapsedTimeString = value;
                PropertyChanged?.Invoke(this, new PropertyChangedEventArgs("ElapsedTimeString"));
            }
        }
    }

    // ....
}

и XAML:

<TextBlock Name="tbElapsedTime" Text="{Binding ElapsedTimeString, RelativeSource={RelativeSource Self}}"/>

Привязка данных часто используется в сочетании с MVVM. Это ИМХО предпочтительный способ решения вашей проблемы... Если вы хотите использовать MVVM, вам необходимо реализовать модель представления, которая содержит всю логику и реализует INotifyPropertyChanged. Затем вы можете просто привязать свойства из модели представления к представлению. Это обеспечивает хорошее разделение между логикой (связанной с графическим интерфейсом) и представлением.

person JanDotNet    schedule 28.07.2017
comment
Извините за предыдущий комментарий, во время тестирования я забыл удалить код, который напрямую обновляет значение текстового поля. Я использовал ваш метод и привязку, но он все еще не работает. TextBlock просто остается пустым. - person Sach; 28.07.2017
comment
Конечно.... RelativeSource=Self использует TextBox в качестве источника привязки. Вы можете изменить относительный источник, чтобы он стал окном, или... - person JanDotNet; 29.07.2017
comment
Задайте для свойства DataContext окна экземпляр окна и удалите определение относительного источника из привязки. - person JanDotNet; 29.07.2017
comment
Завтра могу выложить пример... ;) - person JanDotNet; 29.07.2017
comment
Я только что понял это; в моем коде я установил this.DataContext=this в событии загрузки окна, и теперь он работает. - person Sach; 29.07.2017