Wymiana danych w aplikacji C# WPF z użyciem Caliburn.micro

0

Witam, tworze sobie aplikację w C# WPF z użyciem Caliburn.micro, aktualnie mam Textblock i jeden button, który po kliknięciu zmienia text w Texblocku - teraz chciałbym zmienić wartość Textblocku w nowym wątku, który uruchomię czy ktoś mógłby podpowiedzieć jak to zrobić?

Mianowicie mam coś takiego:

a) Startowanie programu

    public class Bootstrapper : BootstrapperBase
    {
        public Bootstrapper()
        {
            Initialize();
        }

        protected override void OnStartup(object sender, StartupEventArgs e)
        {
            DisplayRootViewFor<ShellViewModel>();
        }

    }

b) ViewModels: (ShellViewModel):

    public class ShellViewModel : Screen
    {
        private string _message = 2;

        public int Message
        {
            get { return _message; }
            set { _message = value; NotifyOfPropertyChange(() => Message); }
        }

        public void setNewText()
        {
            Message = "zmienilem tekst na nowy";
        }
    }

I Views: (ShellView.xaml):

    <Grid>
        <TextBlock Text="{Binding Path=Message, Mode=OneWay}" HorizontalAlignment="Left" Margin="355,10,0,0" TextWrapping="Wrap" VerticalAlignment="Top"/>
        <Button x:Name="setNewText" HorizontalAlignment="Left" VerticalAlignment="Top" Width="75" Margin="10,49,0,0">Dodaj</Button>

    </Grid>
0

Nie wiem czym jest caliburnmicro ,ale w wpfie by zmienić jakiś element programu w inny wątku trzeba użyć Dispatchera https://docs.microsoft.com/pl-pl/dotnet/api/system.windows.threading.dispatcher?view=netcore-3.1

Poczytaj też to https://stackoverflow.com/questions/661561/how-do-i-update-the-gui-from-another-thread

0

@Botek: zaczynając już od .NET 3.5 wszystkie eventy INotifyPropertyChanged są automatycznie dispatchowane przez framework więc nie trzeba używać Dispatchera ręcznie. Dla kolekcji od 4.5 możesz włączyć https://docs.microsoft.com/en-us/dotnet/api/system.windows.data.bindingoperations.enablecollectionsynchronization?view=netcore-3.1

@Paweł Kurzepa Podany przykład powinien działać, upewnij się że setNewText jest wywoływane (ustaw breakpoint). Możliwe że Caliburn nawalił podpinając ten event. Pokaż prawdziwy kod bo ten by się nawet nie skompilował...

private string _message = 2;

0

@obscurity: Tak jak napisałeś framework dispatchuje automatycznie czego wczoraj nie byłem świadomy i dziś już wiem, że mój problem polega na zupełnie czym innym.
Metoda setNewText jak najbardziej się wykonuje, ale chciałbym uzyskać dostęp do ShellViewModel z innego wątku.

Czyli po kolei:

  1. Tworze klasę, która wymieni wartość zmiennej Message (ale metodę chce uruchomić w nowym wątku):
public class Changer {
	public void changeMessage() {
		**// I TUTAJ NIE WIEM JAK SIĘ DOBRAĆ DO KLASY ShellViewModel **
		ShellViewModel.setNewText = "zmieniam wartosc message";
	}
}
  1. Uruchamiam nowy wątek przy starcie programu:
	public class Bootstrapper : BootstrapperBase
    {
        public Bootstrapper()
        {
            Initialize();
        }

        protected override void OnStartup(object sender, StartupEventArgs e)
        {
            DisplayRootViewFor<ShellViewModel>();
			Changer changer = new Changer();
			Thread thr = new Thread(changer.changeMessage);
			thr.Start();
        }

    }

PS. Wiem, że mógłbym utworzyć nowy wątek konstruktorze ShellViewModel i w parametrze do nowego wątku wysłać obiekt this z ShellViewModel, ale nie podoba mi się takie rozwiązanie, bo potrzebuje, a żeby klasa Changer miała w przyszłości również dostęp do method np. z AnotherViewModel...

Tak więc podsumowując moim problemem jest to, że nie wiem jak w innym wątku programu dostać się do klas whatEverViewModel

0

Użyj event aggregatora https://caliburnmicro.com/documentation/event-aggregator
W ten sposób klasy nie muszą o sobie wiedzieć, changer publikuje zmiany, ShellViewModel będzie ich tylko nasłuchiwał

Możesz wysłać tylko stringa jeśli to pojedynczy przypadek, możesz go opakować w klasę (wtedy możesz rozróżniać między różnymi typami wiadomości), albo możesz odwrócić zależność i to ShellViewModel może powiadomić o swoim istnieniu - w tym ostatnim przypadku, jeśli chcesz zapisywać instancje modelu w innej klasie musisz uważać na wycieki pamięci. Jeśli ShellViewModel ma krótszy czas życia niż cała aplikacja to nie przechowuj ich instancji w liście tylko w liście WeakReference (lub możesz użyć ConditionalWeakTable). Ogólnie nie polecam używania event aggregatora w ten sposób bo jak widzisz wprowadza to dużo komplikacji - powinieneś wysyłać nim tylko wiadomości o krótkim czasie życia (utwórz, przetwórz, zapomnij).

A w najprostszym przypadku jeśli to mały tool, możesz utworzyć statyczną zmienną gdzie będziesz przechowywał główną instancję ShellViewModel i odwoływał się bezpośrednio do niej. Zmienne statyczne to jednak dość krótkowzroczne rozwiązanie i utrudnia testowanie. Wszystko zadziała dobrze bo przecież nigdy nie będziesz potrzebował dwóch instancji głównego okna programu albo dostępu do dwóch baz danych w tym samym czasie, prawda? Do czasu aż będziesz potrzebował...

0

Chyba się delikatnie pogubiłem, gdybyś mógł pomóc będę wdzięczny.

Tworze kontener i event aggregator:

        private readonly SimpleContainer _container =
            new SimpleContainer();

         // ... Other Bootstrapper Config

        protected override void Configure(){
            _container.Singleton<IEventAggregator, EventAggregator>();
        }
  1. Do konstruktora ShellViewModel wyśle event aggregator - tylko jak jeżeli on jest tworzony w sposób:
DisplayRootViewFor<ShellViewModel>();
  1. Nie bardzo wiem jak wysłać ten event aggregator do changera - mam wysłać _container ? Jak wtedy w Changerze odniosę się do ShellViewModel?
2

Tak, wysyłasz Event Aggregator. Możesz (powinieneś) tutaj użyć Dependency Injection. Caliburn.Micro ma prosty container do DI https://caliburnmicro.com/documentation/simple-container
Tworzysz po prostu konstruktor

public ShellViewModel(IEventAggregator eventAggregator) {

i automatycznie dostajesz eventAggregator.
I tak jak pisałem wyżej choć może nie jasno - Changer nie powinien się odnosić do ShellViewModel. Powinieneś utworzyć trzecią klasę, powiedzmy "SetNewTextMessage" i Changer powinien wysłać taką klasę, a ShellViewModel ją odebrać i zmienić tekst sam na sobie.

  1. Żeby dostać Event Aggregator w changerze, możesz:

a) najprościej, najgorzej - użyć containera jako service locatora

IoC.Get<IEventAggregator>();

b) lepiej - użyć property i zbudować go z użyciem containera z klasy głównej:

var changer = new Changer();
_container.BuildUp(changer);

c) najlepiej - zintegrować lepszy kontener, np Autofac i użyć go do zbudowania klasy Changer

0

Zapoznałem się z tematem i mniej więcej jestem w stanie zrobić to tak jak napisałeś wyżej.

Teraz uwaga - mam kolejny wysoki problem z tematu Newbie i zaczynam wątpić w samego siebie :).

Mianowicie w xamlu jak wyżej mam utworzony textblock i button:

        <TextBlock Text="{Binding Path=Message, Mode=OneWay}" HorizontalAlignment="Left" Margin="355,10,0,0" TextWrapping="Wrap" VerticalAlignment="Top"/>
        <Button x:Name="setNewText" HorizontalAlignment="Left" VerticalAlignment="Top" Width="75" Margin="10,49,0,0">Dodaj</Button>

Teraz po kliknięciu w Button wykona się metoda setNewText i wymieni się tekst w Texblocku, ale:

a) po kliknieciu buttona: chciałbym wymienić temu textblockowi kolor tła czyli wykonać coś w stylu: textBlock.background = blablabla i nie mogę dojść do tego jak to zrobić

b) po kliknieciu buttona: chciałbym wymienić textblockowi parametr ,,Style" aktualnie mam w nim Style="{StaticResource success}" po kliknięciu chciałbym aby parametr Style zmienił się na Style="{StaticResource error}"

c) chciałbym wymienić textBlock.background z poziomu klasy Changer

d) chciałbym wymienić textBlock.style z poziomu klasy Changer

Jeżeli ktoś ma jeszcze siłę to czytać i pomagać to z góry dziękuje za wszystkie odpowiedzi.

0

Punkty a/b/c/d możesz osiągnać tylko poprzez zmiane stylu. Osobiście wystawił bym property w ViewModelu. Oczywiście property musi rzucać OnPropertyChange. Natomiast na widoku dodać obsługę
Style.Triggers / DataTriggers. Przykład


<ContentControl Grid.Column="1">
                                <ContentControl.Style>
                                    <Style TargetType="ContentControl">
                                        <Style.Triggers>
                                            <DataTrigger Binding="{Binding Status}" Value="{x:Static models:Foo.Undefined}">
                                                <Setter Property="ContentTemplate" Value="{StaticResource Undefined}" />
                                            </DataTrigger>
                                            <DataTrigger Binding="{Binding Status}" Value="{x:Static models:Foo.InProgress}">
                                                <Setter Property="ContentTemplate" Value="{StaticResource InProgress}" />
                                            </DataTrigger>
                                            </DataTrigger>
                                        </Style.Triggers>
                                    </Style>
                                </ContentControl.Style>
                            </ContentControl>

```
0

To by było proste z zastosowaniem code behind, ale zgodnie ze wzorcem MVVM i Caliburn nie powinieneś się z kodu odwoływać do kontrolek bezpośrednio tylko kontrolki powinny na podstawie danych w ViewModelu zmieniać swoje zachowanie. Zwłaszcza cechy takie jak kolor tła nie powinny leżeć w żadnym modelu tylko w widoku właśnie.
Tak że mówiąc inaczej niczego w widoku podmieniać nie możesz a zwłaszcza z poziomu kodu - kod powinien tylko mówić widokowi swój stan a widok na to reagować, właśnie przy pomocy Triggerów (tak jak wyżej), Bindingu z IValueConverter lub Visual State dla kontrolek (wymaga code behind tak czy inaczej).

Najprostszym i najbardziej zbliżonym rozwiązaniem do code behind (gdzie bezpośrednio manipulujesz kontrolkami) będzie IValueConverter: https://www.wpf-tutorial.com/data-binding/value-conversion-with-ivalueconverter/ - wtedy robisz binding do jakiejś własności powiedzmy State typu enum i reagujesz bezpośrednio zwracając konkretny resource.

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