[WPF][MVVM] Aktualizacja ProgressBar przy zablokowanym wątku UI

0

Witam,

mam problem związany z aktualizacją okna progressbara w momencie wykonywania się "ciężkiego" taska blokującego całkowicie UI. Implementuję aplikację typu CAD. Podstawową funkcjonalnością aplikacji jest możliwość zapisywania/odczytu danych do/z plików XML. Jednakże, ze względu na to, iż renderowanie kontrolek (tym bardziej złożonych) w WPF jest niesamowicie czasochłonne postanowiłem informować użytkownika o aktualnym stanie aplikacji. Bardzo powszechnym przypadkiem użycia aplikacji jest wczytywanie schematu, który zawiera kilkanaście tysięcy elementów technicznych. Tego typu operacja powoduje zamrożenie wątku UI na ok. 1.5 minuty. Wczytywanie pliku XML zajmuje ok. 1s.

Chciałbym aby progressbar nie był blokowany w takim scenariuszu i aby była możliwość anulowania wykonywania operacji poprzez kliknięcie odpowiedniego przycisku na widoku ProgressBar'a. Aktualizacja stanu progressbara powinna odbywać się w momencie wywołania metody zdarzeniowej Loaded. Rozgłaszanie informacji o aktualizacji danych następuje poprzez obiekty Rx.NET.

Poniższy kod w momencie wywołania innego taska niż taki, który operuje na UI powoduje bezproblemowe aktualizowanie paska postępu.

W jaki sposób można zrealizować taką funkcjonalność? Niestety po przeczytaniu wielu artykułów dot. tego wątku nadal nie potrafię rozwiązać tego problemu.

Z góry dziękuję za pomoc!

MainWindowViewModel - odpowiada za wczytanie pliku schematu XML, wyświetlenie widoku ProgressBar'a, obsługę ewentualnego anulowania akcji.

public class MainWindowViewModel : WindowViewModel
{
        private readonly IActiveSchema activeSchema;
        private readonly IFrameworkDialogService frameworkDialogService;
        private readonly IObservableProgressBarService observableProgressBarService;

	public ICommand OpenSchemeDialogCommand { get; set; }

        public MainWindowViewModel(
            IActiveSchema activeSchema, 
            IFrameworkDialogService frameworkDialogService, 
            IObservableProgressBarService observableProgressBarService)
        {
            this.activeSchema = activeSchema;
            this.frameworkDialogService = frameworkDialogService;
            this.observableProgressBarService = observableProgressBarService;
            InitializeCommands();
        }

        private void InitializeCommands()
        {
            OpenSchemeDialogCommand = new RelayCommand(param => OpenSchemeDialog());
        }

        private async void OpenSchemeDialog()
        {
            string fileName = frameworkDialogService.ShowOpenFileDialog("Pliki schematów programu (*.abc) | *.abc;");

            var tokenSource = new CancellationTokenSource();
            observableProgressBarService.ShowProgressBarView();
            observableProgressBarService.SetMainProgressBarMaximumValue(1000);
            observableProgressBarService.SetCancellationTokenSource(tokenSource);
            try
            {
                await Open(fileName, tokenSource);
            }
            catch (OperationCanceledException)
            {
            }
            finally
            {
                observableProgressBarService.CloseProgressBarView();
            }           
        }

        private async Task Open(string fileName, CancellationTokenSource tokenSource)
        {
                await Task.Run(() =>
                {
                    //Wczytywanie danych z pliku XML, dodawanie VM kontrolek do ObservableCollection klasy schematu tech.
                    Application.Current.Dispatcher.Invoke(() => 
                        activeSchema.OpenScheme(readerService.Read().ReadScheme(fileName)));
                }, tokenSource.Token);
        }
}

Kod kontrolki wczytywanej z pliku oraz aktualizacja stanu progressbara;

    public class ElectricalElementViewModel : SchemeElementViewModel
    {
        private readonly IObservableProgressBarService observableProgressBarService;
        public ICommand LoadedCommand { get; set; }

        protected ElectricalElementViewModel(IObservableProgressBarService observableProgressBarService)
        {
            this.observableProgressBarService = observableProgressBarService;
            LoadedCommand = new RelayCommand(p => Loaded());
        }

        private async Task Loaded()
        {
            Application.Current.Dispatcher.Invoke(() => observableProgressBarService.UpdateMainProgressBar("Rendering"));
        }
    }

Klasa odpowiedzialna za rozgłaszanie subskrybentom potrzeby aktualizacji wartości progressbara.

    public class ObservableProgressBarService : IObservableProgressBarService
    {
        private Window progressBarView;
        private readonly IDialogService dialogService;
        private readonly Subject<string> mainProgressMessageSubject = new Subject<string>();
        private readonly Subject<int> mainProgressBarMaxValueSubject = new Subject<int>();
        private readonly Subject<CancellationTokenSource> progressCancellationTokenSourceSubject = new Subject<CancellationTokenSource>();

        public IObservable<string> MainObservable => mainProgressMessageSubject;
        public IObservable<int> MainProgressMaximumObservable => mainProgressBarMaxValueSubject;
        public IObservable<CancellationTokenSource> CancellationTokenSourceObservable => progressCancellationTokenSourceSubject;

        public ObservableProgressBarService(IDialogService dialogService)
        {
            this.dialogService = dialogService;
        }

        public void UpdateMainProgressBar(string name)
        {
            mainProgressMessageSubject.OnNext(name);
        }

        public void SetMainProgressBarMaximumValue(int value)
        {
            mainProgressBarMaxValueSubject.OnNext(value);
        }

        public void SetCancellationTokenSource(CancellationTokenSource source)
        {
            progressCancellationTokenSourceSubject.OnNext(source);
        }

        public void ShowProgressBarView()
        {
            progressBarView = dialogService.Show(WindowType.ProgressBarView);
        }

        public void CloseProgressBarView()
        {
            progressBarView?.Close();
            progressBarView = null;
        }
    }

ViewModel progressbara. Aby nie wydłużać postu, accessory właściwości bindowanych do widoku zostały usunięte.

    public class ProgressBarViewViewModel : WindowViewModel, IProgressBarViewViewModel
    {
        private readonly IObservableProgressBarService observableProgressBarService;

        private int mainProgressBarValue;
        private string mainProgressBarMessage;
        private int mainProgressBarMaximum;

        public int MainProgressBarValue { get; set; }
        public string MainProgressBarMessage { get; set; }
        public int MainProgressBarMaximum { get; set; }

        public ICommand CancelTaskCommand { get; set; }
        public CancellationTokenSource CancellationTokenSource { get; set; } = new CancellationTokenSource();

        public ProgressBarViewViewModel(IObservableProgressBarService observableProgressBarService)
        {
            this.observableProgressBarService = observableProgressBarService;
            InitializeCommands();
            InitializeObservers();
        }

        private void InitializeCommands()
        {
            CancelTaskCommand = new RelayCommand(p => CancelTask(), p => CanCancelTask());
        }

        private void InitializeObservers()
        {
            observableProgressBarService.MainObservable.Subscribe(UpdateMainProgressBar);
            observableProgressBarService.MainProgressMaximumObservable.Subscribe(UpdateMainProgressBarMaximum);
            observableProgressBarService.CancellationTokenSourceObservable.Subscribe(SetCancellationTokenSource);
        }

        private void UpdateMainProgressBar(string message)
        {
            MainProgressBarMessage = message;
            MainProgressBarValue += 1;
        }

        private void UpdateMainProgressBarMaximum(int maximum)
        {
            MainProgressBarMaximum = maximum;
        }

        private void SetCancellationTokenSource(CancellationTokenSource source)
        {
            CancellationTokenSource = source;
        }

        private void CancelTask()
        {
            CancellationTokenSource?.Cancel();
        }

        private bool CanCancelTask() => !CancellationTokenSource.IsCancellationRequested;
    }
1

Jeśli progress bar jest w tym samym okienku w którym tworzysz te tysiące kontrolek, to sytuacja wygląda kiepsko i można próbować pewnych sztuczek by w miarę responsywnie się zachowywał progress bar, ale i tak UX będzie fatalne.

Jeśli progress bar znajduje się w oddzielnym okienku wyświetlanym ponad, no to trzeba to okienko uruchomić w oddzielnym wątku i będzie to pięknie śmigać.

A tak pzt, to do renderowania tysięcy elementów w WPF raczej się nie używa kontrolek tylko drawing visuals, których spokojnie można tworzyć tysiące w ciągu sekund, a nie minut.

0

Progress bar jest w osobnym widoku.
Źle się wyraziłem, miałem na myśli oczywiście DrawingVisuale, które są zdecydowanie lżejsze. Natomiast czy to ich renderowanie zajmuje sekundy... W moim przypadku renderowanie ok. 15k złożonych obiektów DrawingVisual zajmuje ok. 80s.

0

Widok jest pojęciem abstrakcyjnym i nic mi to nie mówi. Ważne jest czy te widoki są wyświetlane w tym samym oknie czy nie.

0

Widoki są wyświetlane w osobnych oknach.

0

Ok, utworzenie okna w odrębnym wątku poskutkowało, wedle zaleceń kolegi @neves. Jednakże w związku z tym pojawił się inny problem związany z anulowaniem zadania. Po dodaniu metody ThrowIfCancellationRequested w CancelTaskCommand, program zgłasza wyjątek w bloku try-catch wątku. Komenda pozbawiona tej metody wywoływanej na tokenie nie powoduje anulowania zadania.

        public void ShowProgressBarView(CancellationTokenSource tokenSource)
        {
            var thread = new Thread(p =>
            {
                var token = (CancellationTokenSource) p;
                try
                {
                    progressBarView = windowResolver(WindowType.ProgressBarView);

                    progressBarView.WindowStartupLocation = WindowStartupLocation.CenterScreen;
                    progressBarView.DataContext =
                        factory.GetWindowViewModel(WindowType.ProgressBarView, new object[] {token});
                    progressBarView.Show();
                    Dispatcher.Run();
                }
                catch (OperationCanceledException ex)
                {
                    
                }
            });

            thread.SetApartmentState(ApartmentState.STA);
            thread.IsBackground = true;
            thread.Start(tokenSource);
        }

        public void CloseProgressBarView()
        {
            progressBarView.Dispatcher.Invoke(DispatcherPriority.Normal, new ThreadStart(progressBarView.Close));
            progressBarView = null;
        }

1 użytkowników online, w tym zalogowanych: 0, gości: 1