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