From 20a45e06cd1257481605e33ef4bf2c763d2380f9 Mon Sep 17 00:00:00 2001 From: Marcin Wojas Date: Sat, 16 May 2026 22:55:21 +0200 Subject: [PATCH] =?UTF-8?q?soneta-programming=20-=20redukcja=20duplikat?= =?UTF-8?q?=C3=B3w=20i=20mapa=20skilla?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit SKILL.md odchudzony z 493 do 333 linii: sekcje Serwisy, Tłumaczenia i Log wyciągnięte do nowych referencji, sekcja Session skrócona do fundamentów (szczegóły w session-login.md), dodana Mapa skilla. W examples.md usunięte duplikaty z context.md i datapack-guidedrow.md, dodane spisy treści do dużych referencji. Co-Authored-By: Claude Opus 4.7 (1M context) --- soneta-programming/SKILL.md | 254 ++++-------------- .../references/action-result.md | 14 + soneta-programming/references/context.md | 29 ++ .../references/datapack-guidedrow.md | 40 ++- soneta-programming/references/examples.md | 184 ++----------- soneta-programming/references/services.md | 69 +++++ .../references/session-login.md | 17 ++ .../references/translations-logging.md | 70 +++++ 8 files changed, 287 insertions(+), 390 deletions(-) create mode 100644 soneta-programming/references/services.md create mode 100644 soneta-programming/references/translations-logging.md diff --git a/soneta-programming/SKILL.md b/soneta-programming/SKILL.md index 4638094..9fb96e7 100644 --- a/soneta-programming/SKILL.md +++ b/soneta-programming/SKILL.md @@ -5,22 +5,36 @@ description: > obiektowo-relacyjne (Row, Table, Module), zarządzanie sesją (Session), logowanie (Login, Database, BusApplication), paczki danych (Datapack, GuidedRow) oraz kontekst (Context). Używaj gdy użytkownik pyta o podstawowe klasy logiki biznesowej, - strukturę obiektów ORM, sesje i transakcje, hierarchię klas Row/Table/Module, + strukturę obiektów ORM, sesje i transakcje, hierarchię klas Row/Table/Module, mechanizm Datapack i synchronizację danych, lub kontekst aplikacji enova365. --- # Soneta Programming Basics - Podstawowe klasy ORM -Skill zawiera dokumentację fundamentalnych klas logiki biznesowej platformy enova365/Soneta Enterprise. Klasy te -stanowią podstawę mapowania obiektowo-relacyjnego (ORM) i są niezbędne do tworzenia kodu i dodatków. +Skill zawiera dokumentację fundamentalnych klas logiki biznesowej platformy enova365/Soneta Enterprise. Klasy te stanowią podstawę mapowania obiektowo-relacyjnego (ORM) i są niezbędne do tworzenia kodu i dodatków. + +## Mapa skilla + +SKILL.md zawiera "duży obraz" - hierarchię klas, thread-safety, kanoniczne wzorce. Po szczegóły konkretnego tematu sięgaj do referencji: + +| Temat | Gdzie szukać | +|---|---| +| Hierarchia ORM, Row / Table / Module, klucze, ISessionable | sekcje poniżej | +| 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) | +| 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) | +| Gotowe wzorce kodu end-to-end (import, CRUD, obsługa błędów) | [references/examples.md](references/examples.md) | ## Architektura warstw ``` -BusApplication.Instance (singleton) - multihreaded +BusApplication.Instance (singleton) - multithreaded └── Database └── Login - └── Session - singlethreaded + └── Session - single-threaded └── Module └── Table └── Row @@ -31,7 +45,7 @@ BusApplication.Instance (singleton) - multihreaded | Poziom | Opis | Przykłady klas | |--------|------|----------------| | **1. Bazowe** | Klasy wspólne dla wszystkich modułów (Soneta.Business.dll) | `Row`, `Table`, `Module`, `Session`, `Context` | -| **2. Generowane** | Klasy generowane przez BusinessGenerator z *.business.xml (sufiksy: Row, Table, Module) | `TowarRow`, `TowarTable`, `TowaryModule` | +| **2. Generowane** | Klasy generowane przez BusinessGenerator z `*.business.xml` (sufiksy: Row, Table, Module) | `TowarRow`, `TowarTable`, `TowaryModule` | | **3. Implementowane** | Klasy konkretne tworzone przez programistę | `Towar`, `Towary` (bez sufiksów) | **BusinessGenerator** jest automatycznie uruchamiany podczas kompilacji dla plików `*.business.xml`. Szczegółowy opis definiowania business.xml znajduje się w skill **soneta-business-xml**. @@ -41,7 +55,7 @@ BusApplication.Instance (singleton) - multihreaded ``` Row (abstrakcyjna) └── GuidedRow (+ Guid, Attachments, ChangeInfos) - └── ExportedRow (+ Exported flag) + └── ExportedRow (+ Exported flag - rozróżnia dane konfiguracyjne od operacyjnych) Table (abstrakcyjna) └── GuidedTable (indeksator po Guid) @@ -51,6 +65,8 @@ Module (abstrakcyjna) └── [NazwaModulu]Module (np. TowaryModule) ``` +Szczegóły GuidedRow / ExportedRow (flaga `Exported`, atrybuty `guided` / `relguided` w business.xml, ChangeInfos, blokady) - patrz [references/datapack-guidedrow.md](references/datapack-guidedrow.md). + ## Thread-safety ### Obiekty single-threaded (NIE współdzielić między wątkami) @@ -68,76 +84,27 @@ Module (abstrakcyjna) ## Klasa Session - fundamenty -Session to kluczowa klasa do zarządzania danymi. **Każda operacja na danych wymaga sesji.** - -### Tworzenie sesji +Session to kluczowa klasa do zarządzania danymi. **Każda operacja na danych wymaga sesji.** Sesja jest single-threaded i implementuje `IDisposable` - zawsze opakowuj w `using`. ```csharp -// Przez Login Session session = login.CreateSession(readOnly: false, config: false, name: "MojaSesja"); ``` -### Typy sesji - -| Typ | ReadOnly | Config | Użycie | -|-----|----------|--------|--------| -| Edycyjna operacyjna | false | false | Modyfikacja dokumentów, kartotek | -| ReadOnly operacyjna | true | false | Odczyt danych transakcyjnych | -| Edycyjna konfiguracyjna | false | true | Modyfikacja ustawień | -| ReadOnly konfiguracyjna | true | true | Odczyt konfiguracji | - -**WAŻNE:** W sesji operacyjnej nie można modyfikować obiektów konfiguracyjnych - wymagana jest sesja konfiguracyjna (`config: true`). - -### Transakcje biznesowe (WAŻNE!) - -**Każda zmiana obiektu MUSI być w transakcji biznesowej** otwieranej przez `Session.Logout(editMode: true)`: -- Dodawanie nowych obiektów -- Modyfikacja właściwości (properties) -- Kasowanie obiektów +**Każda modyfikacja obiektu MUSI być w transakcji biznesowej** otwieranej przez `Session.Logout(editMode: true)` - dotyczy dodawania, modyfikacji właściwości oraz kasowania: ```csharp -// Logout(editMode: true) - transakcja edycyjna (można modyfikować) using (var transaction = session.Logout(editMode: true)) { towar.Nazwa = "Zmieniona nazwa"; - transaction.Commit(); // lub CommitUI() -} - -// Logout(editMode: false) - transakcja tylko do odczytu -// (modyfikacje możliwe tylko w zagnieżdżonej transakcji edycyjnej) -using (var readTransaction = session.Logout(editMode: false)) -{ - // odczyt danych... - - // zagnieżdżona transakcja edycyjna - using (var editTransaction = session.Logout(editMode: true)) - { - var cena = towar.Ceny["Hurtowa"]; - cena.Netto = new DoubleCy(100m); - editTransaction.Commit(); - } - - readTransaction.Commit(); // WYMAGANE! Inaczej zmiany z zagnieżdżonych transakcji przepadną + transaction.Commit(); // Commit() w kodzie biznesowym + // transaction.CommitUI(); // CommitUI() w kodzie UI (worker, extender, Command) } +session.Save(); // zapis do bazy - optimistic-lock; konflikty wykrywane tu ``` -**Brak Commit() = automatyczny rollback przy Dispose()** (dotyczy też transakcji tylko do odczytu!) +**Brak Commit() = automatyczny rollback przy Dispose()** - dotyczy też transakcji tylko do odczytu. -### Optimistic locking - -Zmiany wykonywane są w trybie **optimistic-lock**: -- Zmiany kumulują się w sesji -- `Session.Save()` zapisuje wszystkie zmiany razem -- Konflikty wykrywane w momencie zapisu - -### Ważne zasady - -- Session implementuje `IDisposable` - **zawsze wywołuj Dispose()** lub używaj `using` -- Wiele sesji może współistnieć jednocześnie -- Sesja konfiguracyjna używa cache'a (optymalizacja odczytów) -- Sesja operacyjna zawsze czyta z bazy (aktualność danych) -- **Nie mieszaj obiektów z różnych sesji** - użyj `session.Get(obiekt)` aby doczytać obiekt w bieżącej sesji -- ID identyfikuje obiekt w tabeli, może powtarzać się w różnych tabelach +Pełna dokumentacja (typy sesji edycyjna / readonly / konfiguracyjna, transakcje zagnieżdżone, Commit vs CommitUI, optimistic locking, mieszanie obiektów z różnych sesji przez `session.Get(obiekt)`) - patrz [references/session-login.md](references/session-login.md). ## Klasa Module @@ -254,165 +221,35 @@ var tm = TowaryModule.GetInstance(sessionable); ## Kod biznesowy vs UI -Kod biznesowy realizuje operacje logiki biznesowej (jak backend). -Kod UI (fronend) jest odpowiedzialny za prezentację danych i interakcję z użytkownikiem. -Kod biznesowy może być umieszczony w tej samej klasie z kodem UI. -Kod UI to np: +Kod biznesowy realizuje operacje logiki biznesowej (jak backend). Kod UI (frontend) jest odpowiedzialny za prezentację danych i interakcję z użytkownikiem. Kod biznesowy może być umieszczony w tej samej klasie z kodem UI. + +Kod UI to np.: - obiekty `View`, `ViewInfo`, extender - metody sterujące `IsReadOnlyXxx`, `IsVisibleXxxx`, `GetListXxx`, `IsEnabledXxx`, `GetNameXxx`, `GetAppearanceXxx` ### Ważne zasady do stosowania w kodzie biznesowym -- Nie używaj żadnych obiektów kodu UI, w szczególności `View` - zamiast tego możesz użyć `SubTable[condition]` -- Nie należy stosować warunków na prawa dostępu (np `if (Table.AccessRight == AccessRights.Denied) {...}`) +- Nie używaj żadnych obiektów kodu UI, w szczególności `View` - zamiast tego możesz użyć `SubTable[condition]`. +- Nie należy stosować warunków na prawa dostępu (np. `if (Table.AccessRight == AccessRights.Denied) {...}`). -## Serwisy +## Metadane modułów, tabel, kluczy, pól -Pozwalają tworzyć obiekty (komponenty), których czas życia będzie zależał od scope: -- App (BusApplication.Instance) -- Database -- Login -- Session (default - nie trzeba określać w deklaracji) +Dostęp do metadanych obiektów biznesowych jest dostępny przez metody `static` klasy `ApplicationInfo`. -Umieszczając deklarację interface serwisu w assembly wspólnym, pozwalają na udostępnianie serwisów między modułami, -nawet gdy nie ma odpowiedniej referencji. - -* **Tylko serwisy scope Session są single-threded, pozostałe są multi-threaded.** -* Serwis może być `IDisposable`. -* Dla serwisów App, Database, Login nie przechowuj obiektów sesyjnych. -* `[RequireOwnService]` tylko dla serwisów, które nie mogą być nadpisywane. - -### Przykład deklaracji - -```csharp -[assembly: Service(ServiceScope.Login)] - -namespace MyNamespace; - -[RequireServiceScope(ServiceScope.Login)] -public interface IRegistry { - void Method(); -} - -internal sealed class Registry : IRegistry { - public void Method() {} -} -``` - -### Odczytanie serwisu w kodzie - -```csharp -IRegistry registerRequired = login.GetRequiredService(); -IRegistry? registerOptional = login.GetService(); - -foreach (IRegistry registers in login.GetServices()) -{ - -} -``` - -### Użycie serwisu w worker lub extender - obiekt tworzony przez Context.CreareObject() - -```csharp -// Rozwiązanie lepsze -class MyWorker1(IRegistry registry) { -} - -class MyWorker2 { - [Context] - private IRegistry Registry { get; set; } -} -``` - -## Metadane modułów, tabel, kluczy, pól, itp - -Dostęp do metadanych obiektów biznesowych dostępny przez metody `static` klasy `ApplicationInfo`. -Odczytanie informacji o tabli `TableInfo info = ApplicationInfo.GetTableInfo(nazwaTabeli)`. Istnieje tylko jedna -referencja obiektu TableInfo dla tabeli. Można używać `ReferenceEquals`, `Dictionary`, itp. -Odczytanie wszystkich tabel `ApplicationInfo.GetTablesInfo()`, a np tabel dla modułu `ApplicationInfo.GetModuleInfo -(moduleName).TableInfos`. +- Odczyt informacji o tabeli: `TableInfo info = ApplicationInfo.GetTableInfo(nazwaTabeli)`. Istnieje tylko jedna referencja obiektu `TableInfo` dla tabeli - można używać `ReferenceEquals`, `Dictionary`, itp. +- Wszystkie tabele: `ApplicationInfo.GetTablesInfo()`. +- Tabele dla modułu: `ApplicationInfo.GetModuleInfo(moduleName).TableInfos`. ### Wykorzystuj `TableInfo` do weryfikacji tabeli ```csharp Row row1 = ...; Row row2 = ...; -if (row1.Table.TableInfo==row2.Table.TableInfo) { +if (row1.Table.TableInfo == row2.Table.TableInfo) { // Ta sama tabela, nawet gdy różne sesje } ``` -## Tłumaczenie i formatowanie napisów, tekstów i string - -Biblioteka obsługuje słowniki tłumaczące napisy w aplikacji Soneta. -* Tłumaczone napisy muszą używać metody typu string-extender `"napis dotłumaczenia".Translate()`. -* Tłumaczenie tekstów formatowanych przez `"napis {0} z wartością {1}".TranslateFormat(arg0, arg1)`. -* Gdy string ma być zignorowany przez tłumacza, MUSISZ zaznaczyć do metodą `"nie tłumaczymy".TranslateIgnore()`,inaczej błąd kompilacji. -* Jeżeli w metodzie lub klasie jest więcej napisów do zignorowania użyj atrybutu `[TranslateIgnore]`. -* Parametr metody jest ignorowany przez tłumacza, użyj atrybutu `[TranslateIgnore]` na parametrze. - -## Log zmian i obserwowalność - -Używaj standardowy narzędzi do logowania `ILogger`. Użyj `[TranslateIgnore]` w metodzie wywołującej log. -Używaj `logger.IsEnable(LogLevel)` kiedy parametry wymagają dodatkowych operacji. - -### Użycie `ILogger` oraz exception log - -```csharp -class Test -{ - private readonly logger = BusApplication.Instance.GetRequiredService>(); - - public decimal Kwota; - - [TranslateIgnore] - public void Metoda() - { - try { - logger.LogInformation("Wywołanie metody {nazwa}", nameof(Validate)); - if (kwota<0) - logger.LogWarning("Kwota {kwota} nie może być ujemna w metodzie '{metoda}'", Kwota, nameof(Validate)); - } - catch (Exception ex) { - - // Sposób na wrzucenie exception do trace - ex.Log(); - - throw; - } - } -} -``` - -### Śledzenie czasu wykonania z obsługą exception - -```csharp -class Test { - private static readonly ActSource actSource = new(nameof(Test), ActSource.TraceLevel.Default); - - public void Action() { - using var activity = actSource.Start(); - - try { - // Algorytm do śledzenia - } - catch (Exception ex) { - activity.AddExceptionWithError(ex); - throw; - } - } -} -``` - -## Szczegółowa dokumentacja - -- **[references/session-login.md](references/session-login.md)** - BusApplication, Database, Login, Session -- **[references/datapack-guidedrow.md](references/datapack-guidedrow.md)** - Paczki danych, GuidedRow, ExportedRow, synchronizacja -- **[references/context.md](references/context.md)** - Klasa Context, komunikacja UI ↔ logika -- **[references/examples.md](references/examples.md)** - Przykłady kodu i wzorce użycia -- **[references/action-result.md](references/action-result.md)** - Action result zwracane przez worker/extender/Command - (typy obsługiwane przez ResultHandler) i własne handlery - ## Szybki start - wzorce kodu ### Odczyt danych @@ -420,7 +257,7 @@ class Test { ```csharp using (var session = login.CreateSession(readOnly: true, config: false, name: "Odczyt")) { - var tm = session.GetTowary(); // Extension method + var tm = session.GetTowary(); foreach (Towar t in tm.Towary.WgKodu) { Console.WriteLine($"{t.Kod}: {t.Nazwa}"); @@ -434,7 +271,7 @@ using (var session = login.CreateSession(readOnly: true, config: false, name: "O using (var session = login.CreateSession(readOnly: false, config: false, name: "Dodawanie")) { var tm = session.GetTowary(); - + using (var transaction = session.Logout(editMode: true)) { var towar = new Towar(); @@ -443,7 +280,7 @@ using (var session = login.CreateSession(readOnly: false, config: false, name: " towar.Nazwa = "Nowy towar"; transaction.Commit(); } - + session.Save(); } ``` @@ -467,6 +304,8 @@ using (var session = login.CreateSession(readOnly: false, config: false, name: " } ``` +Więcej wzorców (kasowanie, obsługa błędów, pełny import end-to-end) - patrz [references/examples.md](references/examples.md). + ## Konwencje nazewnicze | Element | Konwencja | Przykład | @@ -485,6 +324,7 @@ using (var session = login.CreateSession(readOnly: false, config: false, name: " | Identyfikatory systemowe | **angielski** | `Session`, `Context`, `Row`, `Table`, `Module` | **Można łączyć polski i angielski** w nazwach metod i klas: + ```csharp RetrieveTowary() UpdateKontrahent() diff --git a/soneta-programming/references/action-result.md b/soneta-programming/references/action-result.md index 0667dcd..2b7a45e 100644 --- a/soneta-programming/references/action-result.md +++ b/soneta-programming/references/action-result.md @@ -4,6 +4,20 @@ Akcje (metody worker, akcje extender, handlery Command, callbacki) zwracają **a sterujący tym, co stanie się w UI po wykonaniu logiki biznesowej. Typ zwróconego obiektu decyduje o sposobie obsługi. +## Spis treści + +- [Najważniejsza zasada](#najważniejsza-zasada) +- [Wartości specjalne](#wartości-specjalne) - `null`, `string`, `bool` +- [Sterowanie aktualnym formularzem](#sterowanie-aktualnym-formularzem) - close, refresh +- [Okna informacyjne i pytania do użytkownika](#okna-informacyjne-i-pytania-do-użytkownika) - MessageBox, potwierdzenia +- [Otwieranie obiektów i okien](#otwieranie-obiektów-i-okien) - edycja, podgląd +- [Nawigacja po programie](#nawigacja-po-programie) - listy, foldery, URL +- [Pliki i strumienie](#pliki-i-strumienie) - pobieranie plików, podgląd +- [Raporty](#raporty) - generowanie i parametry raportów +- [Dialogi parametrów](#dialogi-parametrów) - kreatory, dialog z ContextBase +- [Operacje klienta i bezpieczeństwo](#operacje-klienta-i-bezpieczeństwo) +- [Tabela szybkiego wyboru](#tabela-szybkiego-wyboru) - mapa typ → efekt UI + ## Najważniejsza zasada **Nie wywołuj sam UI z poziomu worker/extender.** Zamiast pokazywać MessageBox, otwierać formularz diff --git a/soneta-programming/references/context.md b/soneta-programming/references/context.md index 1306cf4..8a91435 100644 --- a/soneta-programming/references/context.md +++ b/soneta-programming/references/context.md @@ -309,6 +309,35 @@ public class TowarExtender } ``` +## 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 diff --git a/soneta-programming/references/datapack-guidedrow.md b/soneta-programming/references/datapack-guidedrow.md index a08a5b4..1764f7e 100644 --- a/soneta-programming/references/datapack-guidedrow.md +++ b/soneta-programming/references/datapack-guidedrow.md @@ -19,20 +19,7 @@ Faktura bez pozycji nie jest kompletną fakturą - wszystkie te obiekty stanowi ## Hierarchia klas Row i Table -``` -Row (abstrakcyjna) Table (abstrakcyjna) - │ └── ID: int └── GuidedTable (indeksator po Guid) - │ └── State: RowState └── ExportedTable - │ - └── GuidedRow - │ └── Guid: System.Guid - │ └── Attachments - │ └── FirstChangeInfo, LastChangeInfo - │ └── Note - │ - └── ExportedRow - └── Exported: bool -``` +Hierarchię ogólną opisuje [SKILL.md](../SKILL.md#hierarchia-głównych-klas). W skrócie: `Row → GuidedRow → ExportedRow` (po stronie wierszy) oraz `Table → GuidedTable → ExportedTable` (po stronie tabel). Szczegóły właściwości - sekcje poniżej. ## Atrybut guided w business.xml @@ -130,6 +117,31 @@ foreach (Attachment att in row.Attachments) Attachment img = row.DefaultImage; ``` +### Dodawanie załącznika z transakcją + +```csharp +public void DodajZalacznik(Login login, Towar towar, byte[] plik, string nazwa) +{ + using (var session = login.CreateSession(false, false, "DodawanieZalacznika")) + { + // Doczytaj towar w bieżącej sesji (mieszanie obiektów z różnych sesji jest błędem) + var towarInSession = session.Get(towar); + + using (var transaction = session.Logout(editMode: true)) + { + var attachment = new Attachment(towarInSession, AttachmentType.Attachments); + towarInSession.Module.Business.Attachments.AddRow(attachment); + + attachment.Name = nazwa; + attachment.RawData = plik; + + transaction.Commit(); + } + session.Save(); + } +} +``` + ## ExportedRow Rozszerza `GuidedRow` o flagę `Exported`. diff --git a/soneta-programming/references/examples.md b/soneta-programming/references/examples.md index 0989b0f..efe748f 100644 --- a/soneta-programming/references/examples.md +++ b/soneta-programming/references/examples.md @@ -2,6 +2,19 @@ Praktyczne przykłady użycia podstawowych klas logiki biznesowej enova365/Soneta. +## Spis treści + +- [Ważne zasady](#ważne-zasady) - thread-safety, transakcje, mieszanie sesji +- [Dostęp do danych](#dostęp-do-danych) - iteracja po kluczach, wyszukiwanie, filtrowanie +- [Tworzenie obiektów](#tworzenie-obiektów) - AddRow, walidacja, AddingRow +- [Modyfikacja obiektów](#modyfikacja-obiektów) +- [Usuwanie obiektów](#usuwanie-obiektów) +- [Praca z kontekstem](#praca-z-kontekstem) → szczegółowo w [context.md](context.md) +- [Praca z GuidedRow](#praca-z-guidedrow) → szczegółowo w [datapack-guidedrow.md](datapack-guidedrow.md) +- [Dane konfiguracyjne vs operacyjne](#dane-konfiguracyjne-vs-operacyjne) - `ExecuteConfig` +- [Pełny przykład - import towarów](#pełny-przykład---import-towarów) +- [Obsługa błędów](#obsługa-błędów) - try/catch wokół transakcji + ## Ważne zasady ### Thread-safety @@ -260,178 +273,11 @@ public void UsunTowar(Login login, string kod) ## Praca z kontekstem -### Worker wyliczający właściwość - -```csharp -// Rejestracja workera na poziomie assembly -[assembly: Worker] - -public class TowarWorker -{ - [Context] - public Magazyn MagazynFiltra { get; set; } - - [Context] - public Towar Towar { get; set; } - - public decimal StanMagazynowy - { - get - { - if (MagazynFiltra != null) - { - return PoliczStan(Towar, MagazynFiltra); - } - return PoliczStanCalkowity(Towar); - } - } - - private decimal PoliczStan(Towar towar, Magazyn magazyn) - { - // Implementacja... - return 0; - } - - private decimal PoliczStanCalkowity(Towar towar) - { - // Implementacja... - return 0; - } -} -``` - -### Akcja workera w menu Czynności - -```csharp -// Rejestracja workera na poziomie assembly -[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) - { - // Implementacja... - } -} -``` - -### Klasa parametrów (ContextBase) - -Klasa `ContextBase` jest przeznaczona do budowania klas parametrów, nie workerów: - -```csharp -public class FiltryTowarow(Context context) : ContextBase(context) -{ - public Magazyn Magazyn { get; set; } - - [Caption("Typ towaru")] // Etykieta w UI, gdy inna niż nazwa property - public TypTowaru? Typ { get; set; } - - public bool TylkoAktywne { get; set; } = true; -} -``` - -**Uwaga:** W klasach parametrów atrybut `[Context]` nie jest wymagany. - -**Współdzielenie wartości przez Context** - wartości parametrów można przechowywać w obiekcie Context, co pozwala na współdzielenie między różnymi klasami parametrów: - -```csharp -public class FiltryTowarow(Context context) : ContextBase(context) -{ - public Magazyn Magazyn - { - get => Context.GetOrDefault(); - set => Context.Set(value); - } - - [Caption("Typ towaru")] - public TypTowaru? Typ - { - get => Context.GetOrDefault(); - set => Context.Set(value); - } -} -``` +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). ## Praca z GuidedRow -### Dostęp do historii zmian - -```csharp -public void PokazHistorie(Session session, Kontrahent kontrahent) -{ - var bm = session.GetBusiness(); - - Console.WriteLine($"Historia zmian dla: {kontrahent.Nazwa}"); - - foreach (ChangeInfo ci in bm.ChangeInfos[kontrahent]) - { - Console.WriteLine($" {ci.Time}: {ci.Operator}"); - } - - // Skróty - Console.WriteLine($"Utworzono: {kontrahent.FirstChangeInfo?.Time}"); - Console.WriteLine($"Ostatnia zmiana: {kontrahent.LastChangeInfo?.Time}"); -} -``` - -### Praca z załącznikami - -```csharp -public void DodajZalacznik(Login login, Towar towar, byte[] plik, string nazwa) -{ - using (var session = login.CreateSession(false, false, "DodawanieZalacznika")) - { - // Doczytaj towar w bieżącej sesji - var towarInSession = session.Get(towar); - - using (var transaction = session.Logout(editMode: true)) - { - var attachment = new Attachment(towarInSession, AttachmentType.Attachments); - towarInSession.Module.Business.Attachments.AddRow(attachment); - - attachment.Name = nazwa; - attachment.RawData = plik; - - transaction.Commit(); - } - session.Save(); - } -} - -public void WyswietlZalaczniki(Towar towar) -{ - foreach (Attachment att in towar.Attachments) - { - Console.WriteLine($"- {att.Name} ({att.RawData.Length} bajtów)"); - } - - // Domyślne zdjęcie - using var defaultImage = towar.DefaultImage; - if (defaultImage != null) - { - Console.WriteLine($"Zdjęcie główne: {defaultImage.FileName}"); - } -} -``` +Przykłady dostępu do historii zmian (`ChangeInfos`) i pracy z załącznikami (dodawanie z transakcją, odczyt, `DefaultImage`) - patrz [datapack-guidedrow.md](datapack-guidedrow.md). ## Dane konfiguracyjne vs operacyjne diff --git a/soneta-programming/references/services.md b/soneta-programming/references/services.md new file mode 100644 index 0000000..6068606 --- /dev/null +++ b/soneta-programming/references/services.md @@ -0,0 +1,69 @@ +# Serwisy biznesowe + +Serwisy pozwalają tworzyć obiekty (komponenty), których czas życia zależy od scope: + +- **App** (BusApplication.Instance) - cały okres działania aplikacji +- **Database** - per baza danych +- **Login** - per zalogowany użytkownik +- **Session** (default - nie trzeba określać w deklaracji) + +Umieszczając deklarację interfejsu serwisu w assembly wspólnym, można udostępniać serwisy między modułami, nawet gdy nie ma między nimi referencji. + +## Zasady + +- **Tylko serwisy scope Session są single-threaded**, pozostałe są multi-threaded. +- Serwis może być `IDisposable`. +- Dla serwisów App, Database, Login **nie przechowuj obiektów sesyjnych** (Session, Row, Module, Table, Context). +- `[RequireOwnService]` tylko dla serwisów, które nie mogą być nadpisywane. + +## Deklaracja + +```csharp +[assembly: Service(ServiceScope.Login)] + +namespace MyNamespace; + +[RequireServiceScope(ServiceScope.Login)] +public interface IRegistry +{ + void Method(); +} + +internal sealed class Registry : IRegistry +{ + public void Method() { } +} +``` + +## Odczyt serwisu + +```csharp +IRegistry registerRequired = login.GetRequiredService(); +IRegistry? registerOptional = login.GetService(); + +foreach (IRegistry registers in login.GetServices()) +{ + // wiele implementacji +} +``` + +Scope odpowiada obiektowi, z którego pobieramy serwis: `BusApplication.Instance.GetRequiredService()`, `database.GetRequiredService()`, `login.GetRequiredService()`, `session.GetRequiredService()`. + +## Użycie w worker / extender + +Obiekt jest tworzony przez `Context.CreateObject()`, więc zależności są wstrzykiwane automatycznie. + +```csharp +// Rozwiązanie lepsze - constructor injection +class MyWorker1(IRegistry registry) +{ + // używaj registry +} + +// Alternatywa - atrybut [Context] na property +class MyWorker2 +{ + [Context] + private IRegistry Registry { get; set; } +} +``` diff --git a/soneta-programming/references/session-login.md b/soneta-programming/references/session-login.md index bbe864a..8156c9e 100644 --- a/soneta-programming/references/session-login.md +++ b/soneta-programming/references/session-login.md @@ -288,6 +288,23 @@ using (var session = login.CreateSession(readOnly: false, config: false, name: " | `transaction.CommitUI()` | Zatwierdza + odświeża UI | | `transaction.Dispose()` | Bez Commit = rollback (także zagnieżdżonych zmian) | +### Commit() vs CommitUI() + +- `Commit()` - używaj w **kodzie biznesowym** (worker bez interakcji UI, kod uruchamiany w tle, testy, kod konsolowy). +- `CommitUI()` - używaj w **kodzie UI** (worker uruchamiany z menu Czynności, extender, Command). Dodatkowo wymusza odświeżenie powiązanych widoków. + +```csharp +[Action("Aktualizuj")] +public void Execute() +{ + using (var transaction = Towar.Session.Logout(editMode: true)) + { + Towar.Nazwa = NowaNazwa; + transaction.CommitUI(); // UI - odśwież listę / formularz + } +} +``` + ## Kompletny przykład ```csharp diff --git a/soneta-programming/references/translations-logging.md b/soneta-programming/references/translations-logging.md new file mode 100644 index 0000000..985171b --- /dev/null +++ b/soneta-programming/references/translations-logging.md @@ -0,0 +1,70 @@ +# Tłumaczenia i logowanie + +Soneta dostarcza dwa fundamenty obserwowalności i lokalizacji, które działają razem - tłumaczenia napisów oraz log zmian / śledzenie czasu wykonania. + +## Tłumaczenie i formatowanie napisów + +Biblioteka obsługuje słowniki tłumaczące napisy w aplikacji Soneta. + +- Tłumaczone napisy muszą używać metody string-extender: `"napis do tłumaczenia".Translate()`. +- Tłumaczenie tekstów formatowanych: `"napis {0} z wartością {1}".TranslateFormat(arg0, arg1)`. +- Gdy string ma być **zignorowany** przez tłumacza, MUSISZ oznaczyć go metodą `"nie tłumaczymy".TranslateIgnore()` - inaczej błąd kompilacji. +- Jeśli w metodzie lub klasie jest wiele napisów do zignorowania, użyj atrybutu `[TranslateIgnore]` na metodzie / klasie. +- Parametr metody ignorowany przez tłumacza - atrybut `[TranslateIgnore]` na parametrze. + +## Log zmian i obserwowalność + +Używaj standardowych narzędzi do logowania `ILogger`. Użyj `[TranslateIgnore]` w metodzie wywołującej log (komunikaty logu nie są tłumaczone). Używaj `logger.IsEnabled(LogLevel)` kiedy parametry wymagają dodatkowych operacji. + +### Użycie `ILogger` oraz exception log + +```csharp +class Test +{ + private readonly ILogger logger = + BusApplication.Instance.GetRequiredService>(); + + public decimal Kwota; + + [TranslateIgnore] + public void Metoda() + { + try + { + logger.LogInformation("Wywołanie metody {nazwa}", nameof(Metoda)); + if (Kwota < 0) + logger.LogWarning("Kwota {kwota} nie może być ujemna w metodzie '{metoda}'", Kwota, nameof(Metoda)); + } + catch (Exception ex) + { + // Sposób na wrzucenie exception do trace + ex.Log(); + throw; + } + } +} +``` + +### Śledzenie czasu wykonania z obsługą exception + +```csharp +class Test +{ + private static readonly ActSource actSource = new(nameof(Test), ActSource.TraceLevel.Default); + + public void Action() + { + using var activity = actSource.Start(); + + try + { + // Algorytm do śledzenia + } + catch (Exception ex) + { + activity.AddExceptionWithError(ex); + throw; + } + } +} +```