Inne argumenty obok wstrzykiwanej zależności w konstruktorze klasy

0

Witam,

mam pewien problem. Myślę i myślę, ale nie mam pomysłu. Chodzi o to, że mam okno z przyciskiem powiedzmy Zobacz szczegóły klienta. I teraz w momencie kliknięcia przycisku metoda wywołuje mi kod:

ClientDetailsWindow clientDetailsWindow = new ClientDetailsWindow();

Wiadomo, że w oknie musi być wyświetlony cały klient, czyli okno musi "wiedzieć" co wyświetlić. I teraz chcę przekazać jakiegoś integera, a w ViewModelu wykorzystując wstrzyknięty przez konstruktor z Unity Containera serwis pobrać klienta do zmiennej. O ile konstruktor mojego ViewModelu wygląda tak:

public ClientDetailsViewModel(IClientService clientService)
        {
            _clientService = clientService;
        }

To ładnie Unity wstrzykuje mi ten serwis i mogę z niego korzystać. Problem pojawia się, że chcę dopisać tutaj drugi parametr typu int. Wtedy, przy tworzeniu obiektu okna muszę podać w argumencie albo tego inta i w konstruktorze okna jakoś wstrzyknąć ten serwis, albo podać nowy obiekt konstruktora i z poziomy okna "matki" jakoś wstrzyknąć ten serwis.

Wiem, może trochę namieszałem opisując, ale może ktoś miał taką sytuację i może się podzielić.

Istnieje rozwiązanie, aby nie wstrzykiwać tam serwisu, tylko do okna od razu w parametrze podać obiekt typu Client. Ale nie o to chodzi.

Póki co mam to zaimplementowane tak, że wstrzykuję sam konstruktor, a Id klienta przed utworzeniem nowego okna zapisuję sobię do pola w mojej klasie statycznej TempValues, a potem w tym nowo otwartym oknie pobieram sobie tę wartość. Ale to nie jest chyba dobre rozwiązanie.

0
  1. Odepnij automatyczne spinanie widoku z viewmodelem: prism:ViewModelLocator.AutoWireViewModel="False";
  2. Zamiast tego w code behind okna wstrzyknij do konstruktora kontener IoC: public SomeWindow(IUnityContainer container);
  3. Utwórz obiekt viewmodelu w konstruktorze z punktu nr 2 tworząc go za pomocą kontenera, a nie zwykłym new, przy okazji wstrzykując swojego inta. Implementacje interfejsów zostaną wstrzyknięte z automatu;
  4. Zrób z tego viewmodelu datacontext vide: DataContext = vm;

Przykład wstrzykiwania własnych argumentów:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using Unity;
using Unity.Injection;
using Unity.Resolution;

namespace IoC
{
    interface ITest
    {
        string TestProperty { get; set; }
    }

    class TestImplementation : ITest
    {
        public string TestProperty { get; set; }
    }

    class MyPrettyViewModel
    {
        public MyPrettyViewModel(ITest testImplementation, int i) { }
    }

    class Program
    {
        static void Main(string[] args)
        {
            var container = new UnityContainer();
            container.RegisterType<ITest, TestImplementation>();

            //  Wstrzykiwanie parametrów:
            var test_value = 19;
            var obj = container.Resolve<MyPrettyViewModel>(new ResolverOverride[] { new ParameterOverride("i", test_value) });
        }
    }
}

Póki co mam to zaimplementowane tak, że wstrzykuję sam konstruktor, a Id klienta przed utworzeniem nowego okna zapisuję sobię do pola w mojej klasie statycznej TempValues, a potem w tym nowo otwartym oknie pobieram sobie tę wartość.

To jest bardzo niedobre rozwiązanie. Lepszym jest wspomniane wstrzyknięcie przez konstruktor, albo zarejestrowanie instancji int'a w kontenerze i odczytanie jej w viewmodelu okna potomnego, kiedy będzie taka potrzeba. Nie będziesz wtedy potrzebować osobnego pola w klasie, specjalnie do przechowywania tego int'a, bo weźmiesz go od razu z kontenerka. Przy rejestrowaniu dobrze jest nazwać sobie jakoś to co wkładasz do kontenera i wyciągać również korzystając z nazwy - nie pomieszają Ci się argumenty.

Kolejną metodą jest wykorzystanie Prism'a i przesłanie pomocniczych danych pod spodem, za pomocą eventów. Tylko tutaj trzeba pamiętać o odpowiedniej kolejności, bo możesz wysłać do okna potomnego event z Id'kiem, w momencie kiedy okno potomne jeszcze nie istnieje i tym samym nie posiada możliwości zasubskrybowania eventu. Informacja pójdzie wtedy w kosmos. :) Jeżeli nie chcesz angażować kontenera i konstrutorów, w których chcesz mieć tylko interfejsy to jest to najlepszy sposób.

0

Ok, ten sposób co na początku opisałeś mi się spodobał. Ale jak tak, patrzę, to piszesz, żeby dać w parametrze konstruktora okna interfejs kontenera. Ale, musiałbym dodać do tego jeszcze jeden parametr, inta id klienta. Pytanie, czy mi się implementacja kontenera załaduje w takim razie.

Póki co rozwiązałem to tak, utworzenie okna po kliknięciu powiedzmy buttona:

ClientDetails clientDetails = new ClientDetails();
                        clientDetails.DataContext = new ClientDetailsViewModel(clientId);

A ViewModel wygląda tak:

public class ClientDetailsViewModel : BindableBase
    {
        [Dependency]
        public IClientService ClientService { get; set; }

        private ClientDTO client;
        public ClientDTO Client
        {
            get { return client; }
            set { SetProperty(ref client, value); }
        }


        public ClientDetailsViewModel(int clientId)
        {
            IUnityContainer container = (UnityContainer)Application.Current.Resources["IoC"];
            ClientService = container.Resolve<IClientService>();
            Client = ClientService.GetClient(clientId);
        }
    }

Co prawda korzystam tutaj z Resources, ale jak patrzę Twój przykład, to wartość temu int-owi nadawałbym w CodeBehind, a u mnie byłoby to jeszcze warstwę niżej.

1

Pytanie, czy mi się implementacja kontenera załaduje w takim razie.

Tak, implementacja kontenera załaduje się z automatu.

Co prawda korzystam tutaj z Resources, ale jak patrzę Twój przykład, to wartość temu int-owi nadawałbym w CodeBehind, a u mnie byłoby to jeszcze warstwę niżej.

To pchnij tego inta Prismem za pomocą EventAggregator'a, bo tak to powstanie niepotrzebne drzewo kaskadowych wywołań konstruktów z przekazywaniem identycznych parametrów. Oczywiście wywołanie container.Resolve w podobny sposób co w poście wyżej dla konstruktora klasy zawierającego IUnityContainer i int'a zadziała też prawidłowo.

Za dużo kombinujesz :)

1

@lukaszek016: masz poniżej przykład jak to może wyglądać:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using Unity;
using Unity.Lifetime;
using Unity.Injection;
using Unity.Resolution;

namespace IoC
{
    class MainWindow
    {
        private object DataContext { get; set; }

        public MainWindow(IUnityContainer container, int testInteger)
        {
            var vm = container.Resolve<MainWindowViewModel>(new ResolverOverride[] { new ParameterOverride("testInteger", testInteger) });
            DataContext = vm;
        }

        public void Show()
        {
            //  dla symulacji powiedzmy, ze wiem jakiego typu jest DataContext.
            var dc = (MainWindowViewModel)DataContext;
            Console.WriteLine($"testInteger: {dc.TestInteger}");
        }
    }

    interface ITest { }
    class TestImplementation : ITest { }

    class MainWindowViewModel
    {
        public int TestInteger { get; private set; }

        public MainWindowViewModel(ITest testImplementation, int testInteger)
        {
            TestInteger = testInteger;
        }
    }

    class Program
    {
        static void Main(string[] args)
        {
            var container = new UnityContainer();
            container.RegisterType<ITest, TestImplementation>();

            var test_int = 19;
            var window = container.Resolve<MainWindow>(new ResolverOverride[] { new ParameterOverride("testInteger", test_int) });
            window.Show();
        }
    }
}

Resolve'ować można nie tylko typy zarejestrowane.

0

Ok dzięki Ci za pomoc! Dam znać jak poszło. Ale wygląda na to, że to jest to czego szukam.

0

No, tylko, że teraz masz tam drabinkę dwóch dziwnych wywołań, miast jednego eventu puszczonego Prismem, niemniej jednak będzie to działać. :-)

0

Wiem, spróbuję wykorzystać EventAggregator, ale jak pewnie pamiętasz to moja pierwsza aplikacja MVVM, także po kolei się uczę wszystkiego w miarę potrzeb, i właśnie dotarłem do EventAggregatora, który z tego, co patrzyłem bardzo ułatwi robotę nie tylko jeśli chodzi o ten konkretny przypadek.

0

Na WPF się nie znam, ale zarówno wstrzykiwanie kontenera do viewmodelu jak i rejestrowanie intów w kontenerze brzmią dla mnie groźnie. Czy viewmodel nie może po prostu wyciągnąć z modelu odpowiednich danych na podstawie znanego sobie ID?

0

wstrzykiwanie kontenera do viewmodelu

Nie wstrzykuję kontenera do VM tylko do klasy wyżej.

rejestrowanie intów w kontenerze brzmią dla mnie groźnie

Z rejestrowaniem trzeba uważać, bo można spowodować nieświadomie wycieki pamięci, to akurat prawda.

Czy viewmodel nie może po prostu wyciągnąć z modelu odpowiednich danych na podstawie znanego sobie ID?

Żeby rozpropagować dane do vm na podstawie Id to najpierw trzeba to Id jakoś mu przekazać. Jak nie przez konstruktor, bo autor np. chce mieć tam same tylko interfejsy, to trzeba jakoś pod spodem to zrobić. Zresztą WPF jest stanowy więc, np. edytując jakiś obiekt można go przekazać w ogóle w całości, nie ładując od nowa z bazy na podstawie Id. Chyba, że chcesz mieć zawsze świeże dane, bo obawiasz się, że inny klient mógł je zmienić.

0

@grzesiek51114: to co jest źródłem ID? I czy to ID nie może zostać użyte przez jakąś "fabrykę" ViewModeli aby utworzyć gotowy obiekt, bez przekazywania intów w konstruktorze, bo to naprawdę zły pomysł. Nie przez wycieki pamięci tylko przez to, że kontenery powinny zajmować się wstrzykiwaniem zależności, a ID to są dane, a nie zależność.

0

No jest możliwość: przekazać to ID evetem z event aggregatora z Prism'a. Pisalem o tym wszak ale pytacz jeszcze nie zna dobrze Prisma. ;)

Takie rzeczy przez Prisowe eventy ładnie działają.

0

Grzesiek, jak już zacząłeś temat

Zresztą WPF jest stanowy więc, np. edytując jakiś obiekt można go przekazać w ogóle w całości, nie ładując od nowa z bazy na podstawie Id.

Masz na myśli, jak będę robił window do edycji np. klienta, to mogę przekazać cały obiekt klienta, a potem go zapisać rozumiem ze zmienionymi danymi korzystając z bindowania TwoWay?

A drugie, jak wrzucam obiekt Klienta do ViewModelu z powiedzmy 20 polami, to lepiej zostawić jako obiekt dla widoku i w widoku pisać np.

Text={Binding Client.Name}

Czy lepiej w ViewModelu porozbijać to na właściwości?

0

Masz na myśli, jak będę robił window do edycji np. klienta, to mogę przekazać cały obiekt klienta, a potem go zapisać rozumiem ze zmienionymi danymi korzystając z bindowania TwoWay?

Dokładnie, tylko trzeba pamiętać, że grzebiąc w vm przekazywanego obiektu grzebiemy w oryginale i jak się rozmyślisz w edycji, a wyedytujesz kilka pól to w oryginale także będą zmienione. Ot, klasa jako typ referencyjny ale zawsze można przekazać kopię, tylko do edycji.

Czy lepiej w ViewModelu porozbijać to na właściwości?

W zasadzie wszytko jedno, bo to mała zmiana, a na jedno wychodzi.

0

Hm, teraz napisałbym, że skoro nie zrobię na kontekście SaveChanges i zamknę okno po rozmyśleniu się to nic się nie stanie, ale w gdyby tak było to pewnie byś o tym nie pisał ;)

Ok w razie potrzeby założę nowy temat, ale skoro już tu jesteś, chcę zrobić w głównym oknie na pasku aktualny czas z "lecącymi" sekundami. Dodałem właściwość do zbindowania i napisałem taką metodę, którą odpalam w konstruktorze ViewModelu. Czy to jest ok Twoim/Waszym zdaniem?

private async void TickTime()
        {
            await Task.Run(() =>
            {
                while (true)
                {
                    DateTimeValue = DateTime.Now.ToLongTimeString();
                    Task.Delay(1000);
                }

            });
        }
1

Czy to jest ok Twoim/Waszym zdaniem?

W sumie czemu nie ale sam zrobiłbym to w oparciu o timer i co sekundę aktualizowałbym własność. Tego taska np. nie ma jak przerwać, a Timer już tak. W Twoim przypadku może się zdarzyć, że będziesz odpalać taska wielokrotnie i nawet nie będziesz o tym wiedział. :)

https://msdn.microsoft.com/pl-pl/library/system.timers.timer(v=vs.110).aspx

PS: to nie jest tak, że na dany problem istnieje tylko jedno, najpiękniejsze rozwiązanie. :-) Kiedyś np. w oparciu o Timer bindowałem pasek postępu, pokazujący ile zostało do odświeżenia się jakiegoś tam DataGrid'a. Ten sposób wydawał mi się najlepszy, co nie znaczy, że taki rzeczywiście był... ale działał bez zarzutu.

0

Wiesz co, co do odpalania go wielokrotnie, odpalony byłby w oknie głównym programu i jedyna opcja, aby zamknąć to okno i otworzyć ponownie to wylogowanie się z programu, co skutkuje wywołaniem metody Close na oknie, która chyba zwalnia wszystkie zasoby.

Ale faktycznie, skoro jest gotowe rozwiązanie to nie pozostaje nic jak z niego skorzystać, dzięki ;)

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