soneta-programming - redukcja duplikatów i mapa skilla
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) <noreply@anthropic.com>
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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<WyslijEmailWorker, Kontrahent>]
|
||||
|
||||
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<T>, GetOrDefault<T>, GetRequired<T>** zamiast indeksatora - bezpieczniejsze
|
||||
|
||||
@@ -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`.
|
||||
|
||||
@@ -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<TowarWorker, Towar>]
|
||||
|
||||
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<WyslijEmailWorker, Kontrahent>]
|
||||
|
||||
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<Magazyn>();
|
||||
set => Context.Set(value);
|
||||
}
|
||||
|
||||
[Caption("Typ towaru")]
|
||||
public TypTowaru? Typ
|
||||
{
|
||||
get => Context.GetOrDefault<TypTowaru?>();
|
||||
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
|
||||
|
||||
|
||||
@@ -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<MyNamespace.IRegistry, MyNamespace.Registry>(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>();
|
||||
IRegistry? registerOptional = login.GetService<IRegistry>();
|
||||
|
||||
foreach (IRegistry registers in login.GetServices<IRegistry>())
|
||||
{
|
||||
// wiele implementacji
|
||||
}
|
||||
```
|
||||
|
||||
Scope odpowiada obiektowi, z którego pobieramy serwis: `BusApplication.Instance.GetRequiredService<T>()`, `database.GetRequiredService<T>()`, `login.GetRequiredService<T>()`, `session.GetRequiredService<T>()`.
|
||||
|
||||
## 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; }
|
||||
}
|
||||
```
|
||||
@@ -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
|
||||
|
||||
@@ -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<T>`. 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<Test> logger =
|
||||
BusApplication.Instance.GetRequiredService<ILogger<Test>>();
|
||||
|
||||
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<Test>();
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
Reference in New Issue
Block a user