Update w UI właściwości zmienianej w pętli we wzorcu MVVM

0

Witam, mam taki oto problem:
W WPF:

<Button Content="Generuj zapisu z pliku Excel" Command="{Binding Path=ButtonGenerate}" MinWidth="220" Height="30" Margin="20 5 5 5" VerticalAlignment="Bottom"/>
<TextBox Text="{Binding Path=FilesLeft, Mode=OneWay}"/>

W C#:

 private void ButtonGenerateClick(object obj)
        {
            OpenFileDialog openDialog = new OpenFileDialog();
            openDialog.InitialDirectory = Environment.GetFolderPath(Environment.SpecialFolder.Desktop);
            openDialog.Filter = "Pliki xls|*.xlsx";

            if (openDialog.ShowDialog() == true)
            {
                string directoryPath = Path.GetDirectoryName(openDialog.FileName);
                try
                {
                    using (var excelWorkbook = new XLWorkbook(openDialog.FileName))
                    {
                        var nonEmptyDataRows = excelWorkbook.Worksheet(1).RowsUsed();

                        FilesLeft = nonEmptyDataRows.Count();

                        foreach (var dataRow in nonEmptyDataRows)
                        {
                          // A tutaj odpytywane są API i tworzony jest odpowiedni plik
                            FilesLeft--;

                        }
                    }
                }
                catch (Exception) { };
            }
        }

Czyli na podstawie pliku Excel zawierajacego numery NIP odpytywane sa API w celu poszukiwania informacji o kontrahentach. To troche trwa. Nastepnie dla kazdego numeru NIP jest tworzony osobny plik Excel w tej samej lokaliozacji. Właściwość FilesLeft miała mi wyświetlać w TextBox, prostą informacje o tym ile nipów / plików pozostało jeszcze do sprawdzenia i zapisania. Niestety FilesLeft nie odświeża sie po kazdym pliku a tylko na poczatku i na samym koncu. Wiem już, ze dzieje sie tak dlatego gdyż cała pętla działa w tym samym wątku co główne okno. Moje pytanie jak te wątki rozdzielić tak aby FilesLeft aktualoizowała się w UI po każdym obrocie pętli?

0

Jeśli drugi kod jest częścią ViewModelu to znaczy, że Twój ViewModel jest nieprawidłowy z punktu widzenia MVVM.

0

Witam,

A po czym mam tutaj rozpoznać że to jest MVVM?

Pozdrawiam,

mr-owl

0

Czy w wartości "margin" nie powinny być przecinki? Działa też na spacji? :o

0

Spróbuj tak opakować pętle (pisane na sucho)

Thread thread = new Thread(() =>
{
     foreach (var dataRow in nonEmptyDataRows)
     {
           // A tutaj odpytywane są API i tworzony jest odpowiedni plik
           FilesLeft--;
           OnPropertyChanged(nameof(FilesLeft));
      }
});

thread.SetApartmentState(ApartmentState.STA);
thread.Start();
0

Rozwiązanie @nerdxg jest w gruncie rzeczy poprawne. Cała robota jest wykonywana w osobnym wątku, co odblokowuje wątek GUI i to ma szansę zadziałać. Trudno mi jednak tak na sucho powiedzieć, co się stanie. Być może aplikacja się wywali z komunikatem, że dostęp do GUI z innego wątku.
Prostym rozwiązaniem będzie też wywołanie samego API asynchronicznie. Reszta może się dziać w wątku głównym, bo nie zabiera dużo czasu, grunt żeby samo wywołanie API było asynchroniczne (idealnie cała metoda powinna być asynchroniczna).

Idealnie mogłoby być tak, że w jednej metodzie pobierasz dane od użytkownika (plik z opendialog), a następnie uruchamiasz drugą metodę asynchronicznie (z resztą kodu). W takim przypadku może okazać się niezbędna synchronizacja jeśli chodzi o update FilesLeft. W WinForms służy do tego Invoke, w WPF jest trochę trudniej:

Do VieModela dodaj dispatcher:

        Dispatcher dispatcher;
        public MyViewModel()
        {
            dispatcher = Application.Current.Dispatcher;
        }

A przy updacie FilesLeft:

            var action = new Action(() =>
            {
                FilesLeft--;
            });

            if (dispatcher == null || dispatcher.CheckAccess())
            {
                action();
            }
            else
                dispatcher.Invoke(action);

Nie zdziw się, jak nie zobaczysz w Intellisense metody CheckAccess. Z jakiegoś powodu jest niewidoczna. To oczywiście rozwiązanie, jeśli większość kodu będziesz wykonywał asynchronicznie - nie tylko zapytanie API.

A teraz trochę z innej beczki. Twój kod można by poprawić pod kątem testowalności. Nie powinieneś w ViewModel mieć niczego, co odnosi się bezpośrednio do GUI. Tutaj masz OpenFileDialog. Powinieneś to zrobić interfejsem jakimś. Np. tak:

public interface IFileProvider
{
    string GetFileName();
}

public class WindowsFileProvider: IFileProvider
{
    public string GetFileName()
    {
        OpenFileDialog dialog = new OpenFileDialog();
        //i dalej ustawiasz dialog i pobierasz plik

       return dialog.FileName; //swoją drogą jest lepsze rozwiązanie do pobierania katalogów
    }
}

I dalej w ViewModel przekazujesz obiekt WindowsFileProvider najlepiej przez DeprndencyInjection. Wtedy posługujesz się interfejsem, a nie "konkretnym" obiektem, dzięki czemu możesz później testować ten ViewModel.

0

Ja to robię w następujący sposób:

Tworze klase wykonującą określone operacje w tle posiadającą wymagane zdarzenia do obsługi

public class ProgressEventArgs : EventArgs
    {
        public ProgressEventArgs(int Max, int Value)
        {
            this.Max = Max;
            this.Value = Value;
        }

        public int Max { get; private set; }
        public int Value { get; private set; }
    }
    public class NIPLoader
    {
        public event EventHandler<ProgressEventArgs> ProgressChanged;

        public async void LoadNIPS()
        {
            await Task.Run(() =>
            {
                LoadNIPS(ref ProgressChanged);
            });
        }
        private static void LoadNIPS(ref EventHandler<ProgressEventArgs> ProgressChanged)
        {
            for(int n=0; n<100; ++n)
            {
                ProgressChanged?.Invoke(typeof(NIPLoader), new ProgressEventArgs(99, n));
            }
        }
    }

Tworzę instancję klasy np. w MainWindow.xaml.cs

NIPLoader NIPLoader = new NIPLoader();
private void NIPLoader_ProgressChanged(object sender, ProgressEventArgs e)
{
            Dispatcher?.Invoke(() =>
            {
                ProgressBar1.Max = e.Max;
                ProgressBar1.Value = e.Value;
            });
}

W konstruktorze dodaję obsługę zdarzenia:

NIPLoader.ProgressChanged += NIPLoader_ProgressChanged;
1

Witam,

A nie można na szybko bazować na bibliotece prism i event agregatorze?

Pozdrawiam,

mr-owl

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