diff --git a/soneta-programming/SKILL.md b/soneta-programming/SKILL.md index 9fb96e7..fcb2bd7 100644 --- a/soneta-programming/SKILL.md +++ b/soneta-programming/SKILL.md @@ -23,6 +23,7 @@ SKILL.md zawiera "duży obraz" - hierarchię klas, thread-safety, kanoniczne wzo | Sesje, transakcje, Login, Database, BusApplication, optimistic locking | [references/session-login.md](references/session-login.md) | | Paczki danych, Datapack, GuidedRow, ExportedRow, synchronizacja, blokady | [references/datapack-guidedrow.md](references/datapack-guidedrow.md) | | Klasa Context - dane z UI, zaznaczenia, parametry workera | [references/context.md](references/context.md) | +| Obiekty Worker i Extender - rozszerzenia modelu, akcje w menu Czynności | [references/worker-extender.md](references/worker-extender.md) | | Serwisy biznesowe (App / Database / Login / Session scope) | [references/services.md](references/services.md) | | Tłumaczenia (Translate, TranslateIgnore), ILogger, ActSource | [references/translations-logging.md](references/translations-logging.md) | | Action result zwracany przez worker / extender / Command - raporty, dialogi, nawigacja | [references/action-result.md](references/action-result.md) | diff --git a/soneta-programming/references/context.md b/soneta-programming/references/context.md index 8a91435..ade88ad 100644 --- a/soneta-programming/references/context.md +++ b/soneta-programming/references/context.md @@ -1,4 +1,4 @@ -# Klasa Context +# Klasa Context (kontekst) Dokumentacja klasy Context odpowiedzialnej za komunikację między warstwą logiki biznesowej a interfejsem graficznym. @@ -8,23 +8,25 @@ Context to **kontener par klucz-wartość**, gdzie: - **Klucz** = typ (Type) - **Wartość** = obiekt tego typu (object) -Kontekst jest stale aktualizowany podczas pracy z programem i przechowuje informacje o aktualnym stanie interfejsu. +Context jest stale aktualizowany podczas pracy z programem i przechowuje informacje o aktualnym stanie interfejsu. -## Zawartość kontekstu +## Zawartość context Przykładowa zawartość przy otwartej liście kontrahentów: -| Typ | Opis | -|-----|------| +| Typ | Opis | +|-----|----------------------------------------| | `SelectedCounter` | Liczba zaznaczonych wierszy na gridzie | -| `Kontrahent[]` | Kolekcja zaznaczonych kontrahentów | -| `UILocation` | Aktywny element interfejsu | -| `INavigatorContext` | Kontekst grida (zaznaczenia, focus) | -| `View` | Źródło danych grida | -| `Params` | Klasa parametrów filtrów | -| `Login` | Zalogowany użytkownik | +| `Kontrahent[]` | Kolekcja zaznaczonych kontrahentów | +| `UILocation` | Aktywny element interfejsu | +| `INavigatorContext` | Context grida (zaznaczenia, focus) | +| `View` | Źródło danych grida | +| `Params` | Klasa parametrów filtrów | +| `Session` | Gdy aktywny widok z danymi | +| `Login` | Zalogowany użytkownik | +| `[ViewInfo]+Params` | Klasa parametrów widoku | -## Odczyt z kontekstu +## Odczyt z context ### Metody GetOrDefault i GetRequired (zalecane) @@ -39,16 +41,16 @@ public void Action(Context context) } ``` -### Przez indeksator +### Przez indeksator - gdy typ określony wartością `Type` ```csharp public void Action(Context cx) { - // Rzuca wyjątek gdy brak obiektu w kontekście + // Rzuca wyjątek gdy brak obiektu w context Kontrahent knt = (Kontrahent)cx[typeof(Kontrahent)]; // Bez wyjątku - drugi parametr - Kontrahent knt2 = (Kontrahent)cx[typeof(Kontrahent), false]; + Kontrahent? knt2 = (Kontrahent?)cx[typeof(Kontrahent), false]; } ``` @@ -58,7 +60,7 @@ public void Action(Context cx) public void Action(Context cx) { // Zwraca true jeśli znaleziono, false jeśli nie - if (cx.Get(out DokumentHandlowy dokument)) + if (cx.Get(out DokumentHandlowy? dokument)) { // dokument znaleziony } @@ -69,7 +71,7 @@ public void Action(Context cx) } ``` -## Zapis do kontekstu +## Ustawienie wartości do context ```csharp public void Action(Context cx) @@ -84,135 +86,134 @@ public void Action(Context cx) } ``` -## Zastosowania +## Klasa parametrów (np filtrów) - dziedziczy z `ContextBase` -### 1. Filtry na listach głównych - -Klasy parametrów filtrów dziedziczą z `ContextBase` i są automatycznie w kontekście. +Klasy parametrów filtrów dziedziczą z `ContextBase` ```csharp // Definicja klasy parametrów (w ViewInfo) public class TowaryParams(Context context) : ContextBase(context) { + [Translate] public Magazyn? Magazyn { get => Context.GetOrDefault(); set => Context.Set(value); } + [Translate] public TypTowaru Typ { get => Context.GetOrDefault(); set => Context.Set(value); } [Accessor(AutoChange = true)] + [Caption("Szukaj")] public string SearchString { get; set; } } ``` +* Każde property klasy parametrów dziedziczy z `ContextBase` wymaga kontroli tłumaczenia za pomocą jednego z + atrybutu: `[Translate]`, `[TranslateIgnore]`, `[Caption("Tytuł")]` +* Bindowanie we viewform.xml wewnątrz `FilterPanel` nie wymaga użycia `Context.` (np `TowaryParams`), ponieważ + `Context` jest dostępne bezpośrednio wewnątrz `FilterPanel` +* Bindowanie w pageform.xml wymaga użycia `Context.` (np `Context.TowaryParams`) +* Gdy property nie używa context -> stosuj `[Accessor(AutoChange = true)]` - zamiennie w kodzie set property można + również użyć `Session.InvokeChanged()` lub `Context.InvokeChanged()` + ```xml - - - + + + + + ``` -Wartości filtrów są dostępne przez kontekst dla: +```xml + + + + + + + + +``` + +Wartości filtrów są dostępne przez context do: - Widoków (filtrowanie danych) -- Workerów (właściwości wyliczane) -- Wydruków +- Obiektów worker (właściwości wyliczane, akcje) +- Obiektów extender (właściwości wyliczane) +- Tworzenia obiektów -### 2. Wartości domyślne nowych obiektów +## Tworzenie obiektów -Kontekst używany do inicjalizacji nowych obiektów wartościami z filtrów. Uzupełniane są właściwości zaznaczone atrybutem `[Context]`, który oznacza próbę odczytania wartości do ustawienia property z kontekstu. +Context może tworzyć obiekty worker, extender, i inne obiekty, które mogą być używane w aplikacji. +Do utworzenia obiektu użyj metody `Context.CreateObject()`. -``` -Lista faktur: - Filtr Magazyn: "Sklep" - Filtr Kontrahent: "Drynda" - ↓ -Nowy dokument: - Magazyn: "Sklep" (z kontekstu - property z [Context]) - Kontrahent: "Drynda" (z kontekstu - property z [Context]) -``` +* Parametry konstruktora są wypełniane automatycznie przez context, jeśli są dostępne w kontekście. +* Property oznaczone przez `[Context]` wypełniane są automatycznie przez context wg typu kontekstu. +* Atrybut `[Context(Required=false)]` oznacza, że property nie jest wymagana do tworzenia obiektu. +* Atrybut `[Context("propertyName")]` lub `[Context(typeof(ParamType), "propertyName")]` pozwala na + wypełnienie property z obiektu parametru ze wskazanego property. -### 3. Wydruki - -Wydruki mają dostęp do obiektów z kontekstu jako źródła danych. +### Przykład tworzenia obiektu `Osoba` z parametrów ```csharp -// W kodzie wydruku -Context cx = ...; -if (cx.Get(out Kontrahent[] kontrahenci)) +public class OsobaParams(Context context) : ContextBase(context) { - // kontrahenci[] = zaznaczone na liście -} -``` - -### 4. Workery - atrybut [Context] - -Workery mogą pobierać parametry z kontekstu automatycznie. - -```csharp -public class MojWorker -{ - [Context] // Pobierane z kontekstu - public Magazyn Magazyn { get; set; } + [Translate] + [Accessor(AutoChange = true)] + public string Nazwisko { get; set; } - [Context] // Jeśli brak w kontekście - okno parametrów + [Translate] + [Accessor(AutoChange = true)] + public string Imie { get; set; } + + [Translate] + public Operator Operator { + get => Context.GetOrDefault(); + set => Context.Set(value); + } +} + +// Operator na podstawie wartości Operator z context +public class Osoba(Operator oper) +{ + // Inicjowane na podstawie Nazwisko z obiektu parametru + [Context(nameof(OsobaParams.Nazwisko))] + public string Nazwisko { get; set; } + + // Inicjowane na podstawie Imie z obiektu parametru + [Context(nameof(OsobaParams.Imie))] + public string Imie { get; set; } + + // Jeżeli brak obiektu Kontrahent w context to property jest ignorowane + [Context(Required=false)] public Kontrahent Kontrahent { get; set; } } -``` -### 5. Właściwości wyliczane zależne od filtrów - -```csharp -// Worker wyliczający stan magazynowy -public class StanMagazynu : IWorker +public class OsobaFactory { - public object Compute(Context cx, object source) + public static Osoba Create(Context context) { - Towar towar = source as Towar; - - // Pobranie magazynu z kontekstu (z filtra) - if (cx.Get(out Magazyn magazyn)) - { - return towar.GetStan(magazyn); - } - return towar.GetStanCalkowity(); + return context.CreateObject(); } } ``` -## Klasa ContextBase +## Obiekty Worker i Extender -Bazowa klasa dla obiektów automatycznie umieszczanych w kontekście. +Worker i Extender to dwa mechanizmy rozszerzania modelu o logikę UI - dodatkowe properties wyliczane, akcje w menu Czynności, +pola na formularzu. Oba pobierają parametry z context przez atrybut `[Context]`. -```csharp -public class MojaKlasaParametrow(Context context) : ContextBase(context) -{ - [Accessor(AutoChange = true)] - public Date DataOd { get; set; } - - [Accessor(AutoChange = true)] - public Date DataDo { get; set; } - - public Kontrahent Kontrahent { - get => Context.GetOrDefault(); - set => Context.Set(value); - } -} -``` - -Obiekty dziedziczące z `ContextBase` i nie tylko: -- Są automatycznie dodawane do kontekstu -- Wywoływane jest zdarzenie `OnChanged` przy zmianie właściwości -- Obsługują bindowanie do kontrolek UI -- Właściwości połączone z UI (formularze, parametry) mogą być zadeklarowane z `[Accessor(AutoChange = true)]`, dzięki czemu Accessor automatycznie uruchomi mechanizm powiadamiania o zmianach i nie będzie konieczne wywołanie `Session.InvokeChanged()` lub `Context.InvokeChanged()` +Pełna dokumentacja (rejestracja, deklaracja, bindowanie w form.xml, akcje, przykłady) - +patrz [worker-extender.md](worker-extender.md). ## Interfejs INavigatorContext -Dostępny w kontekście gdy aktywna jest lista (grid). +Dostępny w context gdy aktywna jest lista (grid). ```csharp public void Action(Context cx) @@ -233,7 +234,7 @@ public void Action(Context cx) ## Kolekcje zaznaczonych obiektów -W kontekście znajdują się tablice zaznaczonych obiektów. +W context znajdują się tablice zaznaczonych obiektów. ```csharp public void Action(Context cx) @@ -260,7 +261,7 @@ public void Action(Context cx) ```csharp public void Action(Context cx) { - // Dostęp do sesji przez kontekst + // Dostęp do sesji przez context Session session = cx.Session; // Dostęp do modułu przez sesję @@ -268,94 +269,12 @@ public void Action(Context cx) } ``` -## Pełny przykład - Worker z kontekstem - -```csharp -[assembly: Worker] - -namespace Soneta.Towary; - -public class TowarExtenderParams(Context context) : ContextBase(context) -{ - [Accessor(AutoChange = true)] - [Caption("Magazyn filtrowania")] - public Magazyn MagazynFiltra { get; set; } -} - -public class TowarExtender -{ - [Context] - public TowarExtenderParams Params { get; set; } - - [Context] - public Towar Towar { get; set; } - - public decimal StanWMagazynie => - Params.MagazynFiltra != null - ? PoliczStanMagazynu(Towar, Params.MagazynFiltra) - : PoliczStanMagazynu(Towar); - - private decimal PoliczStanMagazynu(Towar towar, Magazyn magazyn) - { - // Wyliczyć stan we wskazanym magazynie - return 0; - } - - private decimal PoliczStanMagazynu(Towar towar) - { - // Wyliczyć stan w całej firmie - return 0; - } -} -``` - -## Akcja workera w menu Czynności - -Worker może udostępniać akcję uruchamianą z menu - zaznaczone obiekty trafiają do property opisanej `[Context]`: - -```csharp -[assembly: Worker] - -public class WyslijEmailWorker -{ - [Context] - public Kontrahent[] Kontrahenci { get; set; } - - [Context] - public Context Context { get; set; } - - [Action("Wyślij email")] - public void Execute() - { - foreach (var k in Kontrahenci) - { - if (!string.IsNullOrEmpty(k.Email)) - WyslijEmail(k.Email); - } - } - - private void WyslijEmail(string email) { /* ... */ } -} -``` - ## Dobre praktyki 1. **Używaj Get, GetOrDefault, GetRequired** zamiast indeksatora - bezpieczniejsze -2. **Sprawdzaj obecność** obiektów w kontekście przed użyciem +2. **Sprawdzaj obecność** obiektów w context przed użyciem 3. **Dziedzicz z ContextBase** dla własnych klas parametrów i pamiętaj o konstruktorze `(Context context)` -4. **Używaj [Context]** w workerach dla parametrów z kontekstu -5. **Stosuj `[Accessor(AutoChange = true)]`** lub `InvokeChanged()` dla powiadamiania UI o zmianach - -## Typowe typy w kontekście - -| Typ | Kiedy dostępny | -|-----|----------------| -| `Login` | Zawsze po zalogowaniu | -| `Database` | Zawsze po zalogowaniu | -| `LicencjaProgramu` | Zawsze po zalogowaniu | -| `Session` | Gdy aktywny widok z danymi | -| `View` | Gdy aktywna lista | -| `INavigatorContext` | Gdy aktywna lista | -| `[Obiekt][]` | Zaznaczenia na liście | -| `[ViewInfo]+Params` | Klasa parametrów widoku | -| `UILocation` | Lokalizacja w UI | +4. **Stosuj `[Accessor(AutoChange = true)]`** lub `InvokeChanged()` dla powiadamiania UI o zmianach jeżeli wartość + property nie jest przechowywana w context +5. **Używaj `[Translate]`, `[TranslateIgnore]`, `[Caption("Tytuł")]`** dla property klas parametrów (ContextBase) +6. **Worker / Extender** - rozszerzanie modelu o logikę UI, patrz [worker-extender.md](worker-extender.md) diff --git a/soneta-programming/references/examples.md b/soneta-programming/references/examples.md index efe748f..fcd1f66 100644 --- a/soneta-programming/references/examples.md +++ b/soneta-programming/references/examples.md @@ -226,21 +226,21 @@ public void AktualizujCeny(Login login, string nazwaCeny, decimal procentPodwyzk using (var session = login.CreateSession(false, false, "AktualizacjaCen")) { var tm = session.GetTowary(); - - foreach (Towar t in tm.Towary.WgKodu) + + // Jedna transakcja dla całej pętli - szybciej i atomowo + using (var transaction = session.Logout(editMode: true)) { - // Transakcja biznesowa - WYMAGANA dla każdej zmiany! - using (var transaction = session.Logout(editMode: true)) + foreach (Towar t in tm.Towary.WgKodu) { var cena = t.Ceny[nazwaCeny]; if (cena != null) { cena.Netto = new DoubleCy(cena.Netto.Value * (1 + procentPodwyzki / 100)); } - transaction.Commit(); } + transaction.Commit(); } - + session.Save(); // Zapisuje wszystkie zmiany do bazy } } @@ -273,7 +273,7 @@ public void UsunTowar(Login login, string kod) ## Praca z kontekstem -Przykłady Workera z `[Context]`, klasy parametrów dziedziczącej z `ContextBase`, akcji w menu Czynności i współdzielenia wartości przez Context - patrz [context.md](context.md). +Przykłady klasy parametrów dziedziczącej z `ContextBase` i współdzielenia wartości przez Context - patrz [context.md](context.md). Przykłady Workera z `[Context]` i akcji w menu Czynności - patrz [worker-extender.md](worker-extender.md). ## Praca z GuidedRow @@ -440,7 +440,8 @@ public void BezpiecznaOperacja(Login login) { // Logowanie błędu Console.WriteLine($"Błąd: {ex.Message}"); - // Zmiany nie zostały zatwierdzone (brak Commit lub Save) + // Wyjątek przed Commit() = automatyczny rollback transakcji + // Wyjątek z session.Save() = transakcja zatwierdzona w sesji, ale brak zapisu do bazy // Sesja zostanie automatycznie zwolniona przez using } } diff --git a/soneta-programming/references/worker-extender.md b/soneta-programming/references/worker-extender.md new file mode 100644 index 0000000..041465f --- /dev/null +++ b/soneta-programming/references/worker-extender.md @@ -0,0 +1,285 @@ +# Obiekty Worker i Extender + +Obiekty `Worker` i `Extender` rozszerzają model danych o dodatkową logikę UI: +properties wyliczane, akcje w menu Czynności, dodatkowe pola na formularzu. +Oba korzystają z [Context](context.md) do pobierania parametrów. + +## Obiekty Worker + +Dodają dodatkowe properties wyliczane do obiektów, które mogą być stosowane w bindowaniu lub pozwalają na +definiowanie pozycji w menu Czynności. + +* Worker jest zawsze przypisany do obiektu danych. +* W nazwie klasy powinno się stosować sufiks `Worker` +* Nazwa klasy worker powinna określać jego działanie. +* Może być inicjowany z context za pomocą `[Context]` +* Rejestracja za pomocą atrybutu assembly z dwoma parametrami `[Worker]` - zalecana wersja generic + +### Rejestracja worker + +```csharp +// Rejestracja zalecana atrybutem generic +[assembly: Worker] +``` + +```csharp +// Niezalecana rejestracja atrybutem z parametrami +[assembly: Worker(typeof(NazwaKlasyWorker), typeof(DataType))] +``` + +### Deklaracja klasy worker + +```csharp + +[assembly: Worker] + +// Worker wyliczający stan magazynowy +public class WyliczenieStanMagazynuWorker +{ + [Context] + public Magazyn Magazyn { get; set; } + + [Context] + public Towar Towar { get; set; } + + public decimal StanMagazynu => + Magazyn != null + ? Towar.GetStan(Magazyn) + : Towar.GetStanCalkowity(); +} +``` + +Można stosować publiczne metod kontrolujące zachowanie property w edytorze: +* `bool IsVisibleXxx()` - widoczność pola +* `bool IsReadOnlyXxx()` - disable pola +* `object GetListXxx()` - szczegóły edycji + +### Bindowanie na UI form.xml (liście) + +Bindowanie wg schematu: `{Workers..NazwaProperty}` +* Za początku zawsze `Workers.` +* Nazwa typu bez sufiksu `Worker` z nazwy klasy worker (tutaj `WyliczenieStanMagazynu`) + +```xml + + + + + +``` + +### Worker dodający pozycje do menu Czynności w UI + +Worker udostępnia metodę w menu Czynności za pomocą atrybutu `[Action("Tytuł")]`. + +* Jeden worker może udostępniać wiele pozycji (metod) w menu Czynności. +* Metoda Action (w przykładzie metoda `SendEmails`) obiektu worker zwraca [action result](./action-result.md) +* Metoda `bool IsVisibleXxx()` (np `bool IsVisibleSendEmails()`) jest opcjonalna i kontroluje widoczność w menu +* Metoda `bool IsEnabledXxx()` (np `bool IsEnabledSendEmails()`) jest opcjonalna i kontroluje aktywność pozycji w menu +* Metoda `string GetNameXxx()` (np `string GetNameSendEmails()`) jest opcjonalna i kontroluje tytuł pozycji w menu +* Metoda `bool IsCheckedXxx()` (np `bool IsCheckedSendEmails()`) jest opcjonalna i kontroluje zaznaczenie pozycji w menu + +#### Przykład akcji wykonywanej grupowo na liście kontrahentów + +```csharp +[assembly: Worker] + +public class SendEmailsForKontrahentWorker +{ + [Context] + public Kontrahent[] Kontrahenci { get; set; } + + [Action("Wyślij email")] + public object SendEmails() + { + int counter = 0; + foreach (var k in Kontrahenci) + { + if (!string.IsNullOrEmpty(k.Email)) + { + WyslijEmail(k.Email); + ++counter; + } + } + + return "Wysłano {0} emaili.".TranslateFormat(counter); + } + + public bool IsVisibleSendEmails() => Kontrahenci?.Length>0; + public bool IsEnabledSendEmails() => Kontrahenci.All(k => k.Email!=""); + + private void WyslijEmail(string email) { /* ... */ } +} +``` + +#### Przykład akcji wykonywanej pojedynczo na towarze + +```csharp +[assembly: Worker] + +public class KodDuzymiLiteramiWorker +{ + [Context] + public Towar Towar { get; set; } + + [Action("Kod towaru dużymi literami")] + public void MakeUpperName() + { + Towar.Nazwa = Towar.Nazwa.ToUpper(); + } + + public bool IsVisibleMakeUpperName() => Towar != null; + public bool IsEnabledMakeUpperName() => !Towar.Nazwa.IsNullOrEmpty(); +} +``` + +#### Przykład akcji otwierającej formularz kontrahenta dla dokumentu + +```csharp +[assembly: Worker] + +public class PokazKontrahentaDokumentuWorker +{ + [Context] + public DokumentHandlowy Dokument { get; set; } + + [Action("Kontrahent dokumentu")] + public Kontrahent Pokaz() + { + return Dokument.Kontrahent; + } + + public bool IsEnabledPokaz() => Dokument.Kontrahent != null; + + public string GetNamePokaz() => "Pokaż kontrahenta: {0}".TranslateFormat(Dokument.Kontrahent?.Nazwa); +} +``` + +## Obiekty Extender + +Pozwalają na bindowanie logiki interface-owej do formularzy. Można bindować methods i properties z obiektu extender. + +* Extender nie jest przypisany do danych +* W nazwie klasy powinno się stosować sufiks `Extender` +* Może być inicjowany z context za pomocą `[Context]` +* Rejestracja za pomocą atrybutu assembly z jednym parametrem `[Worker]` - zalecana wersja generic + +Można stosować publiczne metod kontrolujące zachowanie property w edytorze: +* `bool IsVisibleXxx()` - widoczność pola +* `bool IsReadOnlyXxx()` - disable pola +* `object GetListXxx()` - szczegóły edycji + +### Rejestracja extender + +```csharp +// Rejestracja zalecana atrybutem generic +[assembly: Worker] +``` + +```csharp +// Niezalecana rejestracja atrybutem z parametrami +[assembly: Worker(typeof(NazwaKlasyExtender))] +``` + +### Deklaracja klasy extender + +```csharp +[assembly: Worker] + +// Extender pokazujący nazwę dużymi literami +public class UpperNazwaExtender +{ + [Context] + public Towar Towar { get; set; } + + public string UpperNazwa + { + get => Towar.Nazwa.ToUpper(); + set => Towar.Nazwa = value.ToUpper(); + } + + public bool IsReadOnlyUpperNazwa() => string.IsNullOrEmpty(Towar.Nazwa); + + public string PokazNazwe() => "Oryginalna nazwa towaru: {0}".TranslateFormat(Towar.Nazwa); +} +``` + +### Bindowanie na UI pageform.xml (formularz) + +Bindowanie wg schematu: `{new .NazwaProperty}` +* Za początku zawsze `new ` +* Nazwa typu z sufiksem `Extender` z nazwy klasy extender (tutaj `UpperNazwaExtender`) +* Podobnie do property, możemy bindować metody: `{new .NazwaMetody()}` + +```xml + + + + + + + +``` + +## Pobieranie parametrów z context - atrybut [Context] + +Worker (i extender) może pobierać parametry z context automatycznie. + +```csharp +public class MojWorker +{ + [Context] // Pobierane z context + public Magazyn Magazyn { get; set; } + + [Context] // Jeśli brak w context - okno parametrów + public Kontrahent Kontrahent { get; set; } +} +``` + +## Pełny przykład - Worker z context + +```csharp +[assembly: Worker] + +namespace Soneta.Towary; + +public class TowarExtenderParams(Context context) : ContextBase(context) +{ + [Accessor(AutoChange = true)] + [Caption("Magazyn filtrowania")] + public Magazyn MagazynFiltra { get; set; } +} + +public class StanTowaruWorker +{ + [Context] + public TowarExtenderParams Params { get; set; } + + [Context] + public Towar Towar { get; set; } + + public decimal StanWMagazynie => + Params.MagazynFiltra != null + ? PoliczStanMagazynu(Towar, Params.MagazynFiltra) + : PoliczStanMagazynu(Towar); + + private decimal PoliczStanMagazynu(Towar towar, Magazyn magazyn) + { + // Wyliczyć stan we wskazanym magazynie + return 0; + } + + private decimal PoliczStanMagazynu(Towar towar) + { + // Wyliczyć stan w całej firmie + return 0; + } +} +``` + +## Dobre praktyki + +1. **Używaj [Context]** w obiektach worker i extender dla parametrów inicjowanych z context +2. **Dziedzicz z ContextBase** dla własnych klas parametrów (patrz [context.md](context.md)) +3. **Metody Action zwracają [action result](./action-result.md)** - nie wywołuj UI bezpośrednio +4. **`CommitUI()` zamiast `Commit()`** - w workerach/extenderach uruchamianych z UI używaj `CommitUI()`