Przemianowałem istnijące skille na prostsze nazwy - porządki

This commit is contained in:
Marcin Wojas
2026-05-16 16:06:36 +02:00
parent e1c3be3846
commit 3e5239cb60
10 changed files with 277 additions and 91 deletions
+332
View File
@@ -0,0 +1,332 @@
# Klasa Context
Dokumentacja klasy Context odpowiedzialnej za komunikację między warstwą logiki biznesowej a interfejsem graficznym.
## Czym jest Context?
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.
## Zawartość kontekstu
Przykładowa zawartość przy otwartej liście kontrahentów:
| 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 |
## Odczyt z kontekstu
### Metody GetOrDefault i GetRequired (zalecane)
```csharp
public void Action(Context context)
{
// Zwraca obiekt lub null, gdy brak obiektu
Kontrahent? knt = context.GetOrDefault<Kontrahent>();
// Zwraca obiekt lub wyjątek, gdy brak obiektu
Kontrahent knt2 = context.GetRequired<Kontrahent>();
}
```
### Przez indeksator
```csharp
public void Action(Context cx)
{
// Rzuca wyjątek gdy brak obiektu w kontekście
Kontrahent knt = (Kontrahent)cx[typeof(Kontrahent)];
// Bez wyjątku - drugi parametr
Kontrahent knt2 = (Kontrahent)cx[typeof(Kontrahent), false];
}
```
### Przez metodę Get<T> (bezpieczna)
```csharp
public void Action(Context cx)
{
// Zwraca true jeśli znaleziono, false jeśli nie
if (cx.Get(out DokumentHandlowy dokument))
{
// dokument znaleziony
}
else
{
// dokument nie znaleziony (dokument = null)
}
}
```
## Zapis do kontekstu
```csharp
public void Action(Context cx)
{
// Przez indeksator z określeniem typu
Kontrahent knt = ...;
cx[typeof(Kontrahent)] = knt;
// Przez metodę Set
DokumentHandlowy dok = ...;
cx.Set(dok);
}
```
## Zastosowania
### 1. Filtry na listach głównych
Klasy parametrów filtrów dziedziczą z `ContextBase` i są automatycznie w kontekście.
```csharp
// Definicja klasy parametrów (w ViewInfo)
public class TowaryParams(Context context) : ContextBase(context)
{
public Magazyn? Magazyn
{
get => Context.GetOrDefault<Magazyn>();
set => Context.Set(value);
}
public TypTowaru Typ {
get => Context.GetOrDefault<TypTowaru>();
set => Context.Set(value);
}
[Accessor(AutoChange = true)]
public string SearchString { get; set; }
}
```
```xml
<!-- Bindowanie w viewform.xml -->
<Field CaptionHtml="Magazyn" EditValue="{TowaryParams.Magazyn}"/>
<Field CaptionHtml="Typ" EditValue="{TowaryParams.Typ}"/>
<Field CaptionHtml="Szukaj" EditValue="{TowaryParams.SearchString}"/>
```
Wartości filtrów są dostępne przez kontekst dla:
- Widoków (filtrowanie danych)
- Workerów (właściwości wyliczane)
- Wydruków
### 2. Wartości domyślne nowych 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.
```
Lista faktur:
Filtr Magazyn: "Sklep"
Filtr Kontrahent: "Drynda"
Nowy dokument:
Magazyn: "Sklep" (z kontekstu - property z [Context])
Kontrahent: "Drynda" (z kontekstu - property z [Context])
```
### 3. Wydruki
Wydruki mają dostęp do obiektów z kontekstu jako źródła danych.
```csharp
// W kodzie wydruku
Context cx = ...;
if (cx.Get(out Kontrahent[] kontrahenci))
{
// 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; }
[Context] // Jeśli brak w kontekście - okno parametrów
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 object Compute(Context cx, object source)
{
Towar towar = source as Towar;
// Pobranie magazynu z kontekstu (z filtra)
if (cx.Get(out Magazyn magazyn))
{
return towar.GetStan(magazyn);
}
return towar.GetStanCalkowity();
}
}
```
## Klasa ContextBase
Bazowa klasa dla obiektów automatycznie umieszczanych w kontekście.
```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<Kontrahent>();
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()`
## Interfejs INavigatorContext
Dostępny w kontekście gdy aktywna jest lista (grid).
```csharp
public void Action(Context cx)
{
if (cx.Get(out INavigatorContext nav))
{
// Wiersz z focusem
object current = nav.Current;
// Zaznaczone wiersze
IEnumerable selected = nav.Selected;
// Liczba zaznaczonych
int count = nav.SelectedCount;
}
}
```
## Kolekcje zaznaczonych obiektów
W kontekście znajdują się tablice zaznaczonych obiektów.
```csharp
public void Action(Context cx)
{
// Zaznaczeni kontrahenci
if (cx.Get(out Kontrahent[] kontrahenci))
{
foreach (var k in kontrahenci)
{
// ...
}
}
// Zaznaczone dokumenty
if (cx.Get(out DokumentHandlowy[] dokumenty))
{
// ...
}
}
```
## Context implementuje ISessionable
```csharp
public void Action(Context cx)
{
// Dostęp do sesji przez kontekst
Session session = cx.Session;
// Dostęp do modułu przez sesję
var tm = session.GetTowary();
}
```
## Pełny przykład - Worker z kontekstem
```csharp
[assembly: Worker<TowarExtender, Towar>]
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;
}
}
```
## Dobre praktyki
1. **Używaj Get<T>, GetOrDefault<T>, GetRequired<T>** zamiast indeksatora - bezpieczniejsze
2. **Sprawdzaj obecność** obiektów w kontekście 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 |
@@ -0,0 +1,245 @@
# Datapack i GuidedRow - Paczki danych
Dokumentacja mechanizmu paczek danych (Datapack) służącego do grupowania powiązanych obiektów i ich synchronizacji między bazami danych.
## Czym jest Datapack?
**Datapack** to struktura obiektów różnych typów powiązanych relacjami, które tworzą logiczną całość.
### Przykład: Dokument handlowy
```
DokumentHandlowy (Root)
├── PozycjaDokHandlowego[] (Child)
├── SumaVAT[] (Child)
└── Platnosc[] (Child)
```
Faktura bez pozycji nie jest kompletną fakturą - wszystkie te obiekty stanowią jedną paczkę danych.
## 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
```
## Atrybut guided w business.xml
Atrybut `guided` w definicji tabeli określa rolę obiektu w strukturze Datapack.
| Wartość | Klasa bazowa | Opis |
|---------|--------------|------|
| `Root` | `GuidedRow` | Główny element paczki danych. Posiada GUID. |
| `Exported` | `ExportedRow` | Jak Root + flaga Exported (czy wyeksportowany) |
| `Child` | `Row` | Element podrzędny w paczce. Musi mieć relację `relguided`. |
| `None` | `Row` | Nie uczestniczy w mechanizmie Datapack |
**Domyślnie:** `guided="Child"`
### Przykłady definicji
```xml
<!-- ROOT - główna kartoteka towaru -->
<table name="Towar" tablename="Towary" guided="Root">
...
</table>
<!-- EXPORTED - dokument do synchronizacji -->
<table name="DokumentHandlowy" tablename="DokHandlowe" guided="Exported">
...
</table>
<!-- CHILD - pozycje dokumentu (domyślnie) -->
<table name="PozycjaDokHandlowego" tablename="PozycjeDokHan">
<col name="Dokument" type="DokumentHandlowy"
relguided="inner" delete="cascade"
children="Pozycje"/>
...
</table>
```
## Atrybut relguided
Określa relację obiektu Child do obiektu Root w strukturze Datapack.
| Wartość | Opis |
|---------|------|
| `inner` | Obiekt podrzędny zagnieżdżony wewnątrz roota w XML |
| `outer` | Obiekt podrzędny poza rootem w XML (ale powiązany) |
| (puste) | Relacja nie jest częścią Datapack |
### Kiedy używać relguided
```xml
<!-- Pozycja dokumentu - CZĘŚĆ datapacka dokumentu -->
<col name="Dokument" type="DokumentHandlowy"
relguided="inner" <!-- powiązanie w Datapack -->
delete="cascade"
children="Pozycje"/>
<!-- Towar na pozycji - NIE jest częścią datapacka dokumentu -->
<col name="Towar" type="Towar" <!-- brak relguided -->
children="Pozycje"/>
```
**Reguły:**
- Tabela Child może mieć **tylko jedną** kolumnę z `relguided`
- Tabela Root **nie posiada** relacji `relguided` (jest korzeniem paczki)
## GuidedRow - szczegóły
### Właściwości
| Właściwość | Typ | Opis |
|------------|-----|------|
| `Guid` | `System.Guid` | Globalnie unikalny identyfikator |
| `Attachments` | `AttachmentCollection` | Kolekcja załączników |
| `DefaultImage` | `Attachment` | Domyślne zdjęcie obiektu |
| `Note` | `string` | Notatka tekstowa |
| `FirstChangeInfo` | `ChangeInfo` | Pierwsza zmiana w historii |
| `LastChangeInfo` | `ChangeInfo` | Ostatnia zmiana w historii |
| `IsAdded` | `bool` | Czy nowo dodany |
| `IsModified` | `bool` | Czy zmodyfikowany |
| `IsDeleted` | `bool` | Czy skasowany |
### Załączniki
Załączniki można podpinać **tylko** do obiektów wywodzących się z `GuidedRow`.
```csharp
GuidedRow row = ...;
// Dostęp do załączników
foreach (Attachment att in row.Attachments)
{
Console.WriteLine(att.Nazwa);
}
// Domyślne zdjęcie
Attachment img = row.DefaultImage;
```
## ExportedRow
Rozszerza `GuidedRow` o flagę `Exported`.
### Użycie
Obiekty, które po eksporcie do systemów zewnętrznych **nie powinny być modyfikowane**.
**Przykład:** Przelew wyeksportowany do systemu bankowego - po zaksięgowaniu nie powinien być zmieniany w ERP.
```xml
<table name="PrzelewBase" tablename="Przelewy" guided="Exported">
...
</table>
```
### Właściwość
| Właściwość | Typ | Opis |
|------------|-----|------|
| `Exported` | `bool` | `true` = obiekt wyeksportowany, nie modyfikować |
## Historia zmian (ChangeInfos)
System loguje zmiany na obiektach `GuidedRow`.
### Tabela ChangeInfos
| Kolumna | Opis |
|---------|------|
| `SourceTable` | Nazwa tabeli źródłowej |
| `SourceGuid` | GUID zmodyfikowanego obiektu |
| `Time` | Data i czas zmiany |
| `Operator` | Kto dokonał zmiany |
### Dostęp do historii
```csharp
var bm = session.GetBusiness();
// Iteracja po historii zmian kontrahenta
Kontrahent knt = ...;
foreach (ChangeInfo ci in bm.ChangeInfos[knt])
{
Console.WriteLine($"{ci.Time}: {ci.Operator}");
}
// Skróty na obiekcie
Console.WriteLine($"Utworzono: {knt.FirstChangeInfo?.Time}");
Console.WriteLine($"Ostatnia zmiana: {knt.LastChangeInfo?.Time}");
```
**Uwaga:** Zmiany na obiektach Child są rejestrowane na poziomie Root.
## Blokady
Modyfikacja **dowolnego** obiektu w strukturze Datapack zakłada blokadę na poziomie **Root**.
```
DokumentHandlowy ← BLOKADA
├── PozycjaDokHandlowego (modyfikacja tutaj)
└── SumaVAT
```
Nie można równolegle edytować obiektów należących do jednego Datapacka.
## GuidedTable
Odpowiednik `GuidedRow` dla tabel - dodaje indeksator po GUID.
```csharp
GuidedTable<Towar> towary = ...;
// Pobranie obiektu po GUID
Guid guid = new Guid("65336878-70cf-4e64-bd72-b742cd26a657");
Towar towar = towary[guid];
```
## Wzorce użycia
### Definiowanie struktury Datapack
```xml
<!-- 1. Root - główny obiekt -->
<table name="Zamowienie" tablename="Zamowienia" guided="Root">
<key name="WgNumeru" keyprimary="true" keyunique="true">
<keycol name="Numer"/>
</key>
<col name="Numer" type="string" length="30" required="true"/>
<col name="Data" type="date" required="true"/>
<col name="Kontrahent" type="Kontrahent" required="true"/>
</table>
<!-- 2. Child - pozycje zamówienia -->
<table name="PozycjaZamowienia" tablename="PozycjeZamowien">
<!-- Jedna relacja relguided="inner" -->
<col name="Zamowienie" type="Zamowienie"
required="true" readonly="true"
relguided="inner" delete="cascade"
children="Pozycje" keyprimary="true"/>
<col name="Lp" type="int" required="true"/>
<col name="Towar" type="Towar" required="true"/>
<col name="Ilosc" type="double" required="true"/>
</table>
```
## Podsumowanie atrybutów
| Atrybut | Gdzie | Wartości | Opis |
|---------|-------|----------|------|
| `guided` | `<table>` | Root, Exported, Child, None | Rola w Datapack |
| `relguided` | `<col>` | inner, outer, (puste) | Relacja Child→Root |
| `delete` | `<col>` | cascade, restrict, setnull | Akcja przy usuwaniu roota |
+643
View File
@@ -0,0 +1,643 @@
# Przykłady kodu - Podstawowe klasy ORM
Praktyczne przykłady użycia podstawowych klas logiki biznesowej enova365/Soneta.
## Ważne zasady
### Thread-safety
**Obiekty single-threaded** - nie współdziel między wątkami:
- `Session`, `Module`, `Table`, `Row`, `Context`
**Obiekty multi-threaded** - można współdzielić:
- `BusApplication`, `Database`, `Login`
Każdy wątek powinien tworzyć własną sesję (Login można współdzielić).
### Extension methods dla modułów
Dostęp do modułów przez extension methods:
```csharp
var tm = session.GetTowary();
var hm = session.GetHandel();
var crm = session.GetCRM();
var kadry = session.GetKadry();
var bm = session.GetBusiness();
```
### Transakcje biznesowe
**Każda zmiana obiektu MUSI być w transakcji** `Session.Logout(editMode: true)`:
```csharp
using (var transaction = session.Logout(editMode: true))
{
// Zmiany: dodawanie, modyfikacja, kasowanie
obiekt.Wlasciwosc = nowaWartosc;
transaction.Commit(); // Zatwierdza zmiany
}
// Brak Commit() = automatyczny rollback
session.Save(); // Zapis do bazy danych
```
## Dostęp do danych
### Odczyt listy towarów
```csharp
using Soneta.Business;
using Soneta.Towary;
public void WyswietlTowary(Login login)
{
// Sesja tylko do odczytu
using (var session = login.CreateSession(true, false, "OdczytTowarow"))
{
var tm = session.GetTowary(); // Extension method
// Iteracja po kluczu podstawowym (WgKodu)
foreach (Towar t in tm.Towary.WgKodu)
{
Console.WriteLine($"{t.Kod}: {t.Nazwa}");
}
}
}
```
### Wyszukiwanie po kluczu
```csharp
public Towar ZnajdzTowar(Session session, string kod)
{
var tm = session.GetTowary();
// Wyszukiwanie po kluczu unikalnym
return tm.Towary.WgKodu[kod];
}
```
### Iteracja z filtrowaniem
```csharp
public void WyswietlAktywneTowary(Session session)
{
var tm = session.GetTowary();
foreach (Towar t in tm.Towary.WgKodu)
{
// Filtrowanie w kodzie
if (t.Typ == TypTowaru.Towar)
{
Console.WriteLine(t.Nazwa);
}
}
}
```
## Tworzenie obiektów
### Dodawanie nowego towaru
```csharp
public void DodajTowar(Login login)
{
// Sesja edycyjna
using (var session = login.CreateSession(false, false, "DodawanieTowaru"))
{
var tm = session.GetTowary();
// Transakcja biznesowa - wymagana!
using (var transaction = session.Logout(editMode: true))
{
// Utworzenie nowego obiektu
var towar = new Towar();
// Dodanie do tabeli (zmiana stanu na Added)
tm.Towary.AddRow(towar);
// Ustawienie właściwości
towar.Kod = "NOWY001";
towar.Nazwa = "Nowy towar";
towar.Typ = TypTowaru.Towar;
transaction.Commit();
}
// Zapisanie do bazy
session.Save();
}
}
```
### Dodawanie dokumentu z pozycjami
```csharp
public void UtworzFakture(Login login, Kontrahent kontrahentZInnejSesji,
List<(Towar towar, int ilosc)> pozycjeZInnejSesji)
{
using (var session = login.CreateSession(false, false, "TworzenieFaktury"))
{
var hm = session.GetHandel();
// WAŻNE: Obiekty z innej sesji trzeba doczytać w bieżącej sesji!
var kontrahent = session.Get(kontrahentZInnejSesji);
// Cała operacja w jednej transakcji
using (var transaction = session.Logout(editMode: true))
{
// Utworzenie nagłówka dokumentu
var faktura = new DokumentHandlowy();
hm.DokHandlowe.AddRow(faktura);
faktura.Definicja = hm.DefDokHandlowe.WgSymbolu["FV"];
faktura.Kontrahent = kontrahent;
faktura.Data = Date.Today;
// Dodanie pozycji
int lp = 1;
foreach (var (towarZInnejSesji, ilosc) in pozycjeZInnejSesji)
{
// Doczytaj towar w bieżącej sesji
var towar = session.Get(towarZInnejSesji);
var poz = new PozycjaDokHandlowego(faktura);
faktura.Pozycje.AddRow(poz);
poz.Towar = towar;
poz.Ilosc = new Quantity(ilosc, towar.Jednostka.Kod);
poz.Lp = lp++;
}
transaction.Commit();
}
session.Save();
}
}
```
**WAŻNE:** W jednej sesji nie można mieszać obiektów z różnych sesji. Użyj `session.Get(obiekt)` aby doczytać obiekt w bieżącej sesji.
## Modyfikacja obiektów
### Aktualizacja pojedynczego obiektu
```csharp
public void ZmienNazweTowaru(Login login, string kod, string nowaNazwa)
{
using (var session = login.CreateSession(false, false, "EdycjaTowaru"))
{
var tm = session.GetTowary();
var towar = tm.Towary.WgKodu[kod];
if (towar != null)
{
using (var transaction = session.Logout(editMode: true))
{
towar.Nazwa = nowaNazwa;
transaction.Commit();
}
session.Save();
}
}
}
```
### Aktualizacja z transakcją biznesową
```csharp
public void AktualizujCeny(Login login, string nazwaCeny, decimal procentPodwyzki)
{
using (var session = login.CreateSession(false, false, "AktualizacjaCen"))
{
var tm = session.GetTowary();
foreach (Towar t in tm.Towary.WgKodu)
{
// Transakcja biznesowa - WYMAGANA dla każdej zmiany!
using (var transaction = session.Logout(editMode: true))
{
var cena = t.Ceny[nazwaCeny];
if (cena != null)
{
cena.Netto = new DoubleCy(cena.Netto.Value * (1 + procentPodwyzki / 100));
}
transaction.Commit();
}
}
session.Save(); // Zapisuje wszystkie zmiany do bazy
}
}
```
## Usuwanie obiektów
### Usuwanie obiektu
```csharp
public void UsunTowar(Login login, string kod)
{
using (var session = login.CreateSession(false, false, "UsuwanieTowaru"))
{
var tm = session.GetTowary();
var towar = tm.Towary.WgKodu[kod];
if (towar != null)
{
using (var transaction = session.Logout(editMode: true))
{
towar.Delete(); // Zmiana stanu na Deleted
transaction.Commit();
}
session.Save(); // Fizyczne usunięcie z bazy
}
}
}
```
## 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);
}
}
```
## 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}");
}
}
```
## Dane konfiguracyjne vs operacyjne
### Odczyt danych konfiguracyjnych
```csharp
// Odczyt pojedynczej wartości
var opisDlaSzt = login.ExecuteConfig(configSession =>
configSession.GetTowary().Jednostki.WgKodu["szt"]?.Opis);
// Odczyt listy wartości prostych
public string[] PobierzKodyJednostek(Login login)
{
return login.ExecuteConfig(configSession =>
{
var tm = configSession.GetTowary();
return tm.Jednostki.WgKodu.Select(j => j.Kod).ToArray();
});
}
```
**WAŻNE:** Używając `ExecuteConfig()` nie można zwracać obiektów sesyjnych z sesji konfiguracyjnej, ponieważ może być używana w innym wątku do innych celów. Zwracaj tylko wartości proste lub kopie danych.
```csharp
public void OdczytKonfiguracji(Login login)
{
// Własna sesja konfiguracyjna - gdy potrzebny dostęp do obiektów
using (var session = login.CreateSession(true, true, "Konfiguracja"))
{
var tm = session.GetTowary();
foreach (Jednostka j in tm.Jednostki.WgKodu)
{
Console.WriteLine($"{j.Kod}: {j.Opis}");
}
}
}
```
### Modyfikacja danych konfiguracyjnych
```csharp
public void DodajJednostke(Login login, string kod, string opis)
{
// Sesja edycyjna konfiguracyjna
using (var session = login.CreateSession(false, true, "DodawanieJednostki"))
{
var tm = session.GetTowary();
using (var transaction = session.Logout(editMode: true))
{
var jednostka = new Jednostka();
tm.Jednostki.AddRow(jednostka);
jednostka.Kod = kod;
jednostka.Opis = opis;
transaction.Commit();
}
session.Save();
}
}
```
## Pełny przykład - import towarów
```csharp
public class ImportTowarow
{
public void Importuj(Login login, string sciezkaPliku)
{
var dane = WczytajZPliku(sciezkaPliku);
using (var session = login.CreateSession(false, false, "ImportTowarow"))
{
var tm = session.GetTowary();
int dodano = 0;
int zaktualizowano = 0;
// Cały import w jednej transakcji
using (var transaction = session.Logout(editMode: true))
{
foreach (var wiersz in dane)
{
// Sprawdź czy towar istnieje
var towar = tm.Towary.WgKodu[wiersz.Kod];
if (towar == null)
{
// Dodaj nowy
towar = new Towar();
tm.Towary.AddRow(towar);
towar.Kod = wiersz.Kod;
dodano++;
}
else
{
zaktualizowano++;
}
// Ustaw/aktualizuj właściwości
towar.Nazwa = wiersz.Nazwa;
var cena = towar.Ceny["Hurtowa"];
cena.Netto = new DoubleCy(wiersz.Cena);
}
transaction.Commit();
}
session.Save();
Console.WriteLine($"Import zakończony:");
Console.WriteLine($" Dodano: {dodano}");
Console.WriteLine($" Zaktualizowano: {zaktualizowano}");
}
}
private List<DaneImportu> WczytajZPliku(string sciezka)
{
// Implementacja wczytywania z CSV/Excel...
return new List<DaneImportu>();
}
private class DaneImportu
{
public string Kod { get; set; }
public string Nazwa { get; set; }
public decimal Cena { get; set; }
}
}
```
## Obsługa błędów
### Wzorzec try-catch z sesją
```csharp
public void BezpiecznaOperacja(Login login)
{
using (var session = login.CreateSession(false, false, "Operacja"))
{
try
{
var tm = session.GetTowary();
using (var transaction = session.Logout(editMode: true))
{
// Operacje na danych...
var towar = new Towar();
tm.Towary.AddRow(towar);
towar.Kod = "TEST";
transaction.Commit();
}
session.Save();
}
catch (Exception ex)
{
// Logowanie błędu
Console.WriteLine($"Błąd: {ex.Message}");
// Zmiany nie zostały zatwierdzone (brak Commit lub Save)
// Sesja zostanie automatycznie zwolniona przez using
}
}
}
```
### Wzorzec z wieloma operacjami
```csharp
public void WieleOperacji(Login login, List<string> kody)
{
using (var session = login.CreateSession(false, false, "WieleOperacji"))
{
var tm = session.GetTowary();
var bledy = new List<string>();
foreach (var kod in kody)
{
try
{
using (var transaction = session.Logout(editMode: true))
{
var towar = tm.Towary.WgKodu[kod];
if (towar != null)
{
towar.Delete();
transaction.Commit();
}
}
}
catch (Exception ex)
{
bledy.Add($"{kod}: {ex.Message}");
// Kontynuuj z następnym elementem
}
}
session.Save(); // Zapisz udane operacje
if (bledy.Any())
{
Console.WriteLine("Błędy: " + string.Join(", ", bledy));
}
}
}
```
@@ -0,0 +1,356 @@
# Session, Login, Database, BusApplication
Dokumentacja klas zarządzających połączeniem z bazą danych i sesjami w platformie enova365/Soneta.
## Hierarchia obiektów
```
BusApplication.Instance (Singleton)
└── Database[] (kolekcja baz danych)
└── Login (uwierzytelniony użytkownik)
└── Session[] (sesje robocze)
└── Module → Table → Row
```
## Klasa BusApplication
Singleton reprezentujący instancję aplikacji ERP. Tworzony podczas inicjalizacji systemu.
### Dostęp do instancji
```csharp
// Singleton
BusApplication app = BusApplication.Instance;
// Dostęp do bazy danych po nazwie
Database db = BusApplication.Instance["BazaDemo"];
// Iteracja po wszystkich bazach
foreach (Database db in BusApplication.Instance)
{
Console.WriteLine(db.Name);
}
```
### Właściwości
| Właściwość | Typ | Opis |
|------------|-----|------|
| `Instance` | `BusApplication` | Statyczny singleton |
| `Is365` | `bool` | `true` = wersja HTML, `false` = wersja okienkowa |
| `this[string]` | `Database` | Indeksator - baza po nazwie |
## Klasa Database
Abstrakcyjna klasa reprezentująca bazę danych. Konkretne implementacje dla wspieranych silników:
```
Database (abstrakcyjna)
└── SqlDatabase (abstrakcyjna)
├── MsSqlDatabase (SQL Server)
└── AzureDatabase (Azure SQL)
```
**Uwaga:** MySqlDatabase oraz OracleDatabase nie są już obsługiwane.
### Konfiguracja
Obiekty Database są deserializowane z pliku `Lista baz danych.xml`.
### Właściwości
| Właściwość | Typ | Opis |
|------------|-----|------|
| `Name` | `string` | Nazwa bazy danych |
| `DefaultDatabase` | `bool` | Czy baza domyślna |
| `DatabaseName` | `string` | Nazwa bazy w silniku SQL (SqlDatabase) |
### Logowanie do bazy
```csharp
Database db = BusApplication.Instance["BazaDemo"];
// ZALECANE: Logowanie z LoginParameters
Login login = db.Login(new LoginParameters
{
UserName = "Administrator",
UserPassword = "password"
});
// Logowanie zintegrowane Windows
Login login = db.Login(new LoginParameters
{
UserName = LoginParameters.WindowsAuthenticationUser
});
```
## Klasa Login
Obiekt reprezentujący zalogowanego użytkownika. Zarządza sesjami. IDisposable.
### Tworzenie
```csharp
Database db = BusApplication.Instance["BazaDemo"];
Login login = db.Login(new LoginParameters
{
UserName = "Administrator",
UserPassword = "password"
});
```
### Dostęp do informacji o operatorze
**WAŻNE:** Właściwości `Operator` i `Entitle` w klasie `Login` nie należy stosować, ponieważ używają obiektu z `ConfigSession` w sposób niekontrolowany.
```csharp
// NIE ZALECANE:
// var op = login.Operator; // unikać!
// var ent = login.Entitle; // unikać!
// ZALECANE: Użyj w konkretnej sesji
using (var session = login.CreateSession(readOnly: true, config: false, name: "Odczyt"))
{
var op = session.AuthorizationInfo.Operator;
Console.WriteLine($"Zalogowany: {op.Name} - {op.FullName}");
}
```
### Właściwości informacyjne
| Właściwość | Typ | Opis |
|------------|-----|------|
| `IsWebUser` | `bool` | Czy operator pulpitu (web) |
| `Licencje` | `IEnumerable` | Pobrane licencje |
| `Database` | `Database` | Baza danych logowania |
### Tworzenie sesji
```csharp
// ZALECANA sygnatura (czytelność + debugowanie)
Session session = login.CreateSession(
readOnly: false, // true = tylko odczyt
config: false, // true = dane konfiguracyjne
name: "MojaSesja" // nazwa sesji - pomocna przy debugowaniu
);
```
**Zawsze używaj wersji z parametrem `name`** - ułatwia debugowanie i identyfikację sesji.
### Dostęp do danych konfiguracyjnych
```csharp
// ZALECANE: ExecuteConfig z lambdą (zwraca wartość prostą)
var opisDlaSzt = login.ExecuteConfig(configSession =>
configSession.GetTowary().Jednostki.WgKodu["szt"]?.Opis);
// NIE ZALECANE: login.ConfigSession
// var configSession = login.ConfigSession; // unikać!
```
**WAŻNE:** Używając `ExecuteConfig()` nie można zwracać obiektów sesyjnych z sesji konfiguracyjnej, ponieważ może być używana w innym wątku do innych celów. Zwracaj tylko wartości proste lub kopie danych.
### Sygnatury CreateSession
```csharp
public Session CreateSession(bool readOnly, bool config, string name) // rekomendowana
public Session CreateSession(bool readOnly, bool config)
public Session CreateSession() // readOnly=false, config=false
```
## Klasa Session
Fundamentalna klasa do zarządzania danymi. **Każda operacja na danych wymaga sesji.** IDisposable.
### Tworzenie
```csharp
// ZAWSZE używaj using lub wywołuj Dispose()
using (var session = login.CreateSession(readOnly: false, config: false, name: "MojaSesja"))
{
// operacje na danych
session.Save();
}
```
### Typy sesji - macierz
| ReadOnly | Config | Zastosowanie |
|----------|--------|--------------|
| `false` | `false` | **Edycja operacyjna** - dokumenty, kartoteki |
| `true` | `false` | **Odczyt operacyjny** - raporty, wyświetlanie |
| `false` | `true` | **Edycja konfiguracyjna** - ustawienia systemu |
| `true` | `true` | **Odczyt konfiguracyjny** - odczyt słowników |
**WAŻNE:** W sesji operacyjnej (`config: false`) nie można modyfikować obiektów konfiguracyjnych. Do modyfikacji konfiguracji wymagana jest sesja konfiguracyjna (`config: true`).
### Właściwości
| Właściwość | Typ | Opis |
|------------|-----|------|
| `ReadOnly` | `bool` | Czy sesja tylko do odczytu |
| `IsConfig` | `bool` | Czy sesja konfiguracyjna |
| `Name` | `string` | Nazwa sesji |
| `Login` | `Login` | Obiekt logowania |
| `Database` | `Database` | Baza danych |
### Metody zarządzania danymi
```csharp
// Zapisanie zmian do bazy
session.Save();
```
### Dane operacyjne vs konfiguracyjne
**Dane operacyjne (`config=false`):**
- Dokumenty, kartoteki, transakcje
- Zawsze odczytywane z bazy (aktualność)
- Modyfikacje zapisywane natychmiast przy `Save()`
**Dane konfiguracyjne (`config=true`):**
- Słowniki, definicje, ustawienia
- Buforowane w cache (optymalizacja)
- Modyfikowane rzadko, odczytywane często
- Określane atrybutem `config="true"` w business.xml
### Wiele sesji jednocześnie
```csharp
// Normalne zjawisko - wiele sesji może współistnieć
using (var session1 = login.CreateSession(readOnly: true, config: false, name: "Lista1"))
using (var session2 = login.CreateSession(readOnly: true, config: false, name: "Lista2"))
{
// Obie sesje mogą odczytywać te same dane
var tm1 = session1.GetTowary();
var tm2 = session2.GetTowary();
}
```
### Transakcje biznesowe - Session.Logout()
**WAŻNE:** Każda zmiana obiektu biznesowego MUSI być w transakcji!
```csharp
using (var session = login.CreateSession(readOnly: false, config: false, name: "Edycja"))
{
var tm = session.GetTowary();
var towar = tm.Towary.WgKodu["KOD001"];
// Logout(editMode: true) - transakcja EDYCYJNA (można modyfikować)
using (var transaction = session.Logout(editMode: true))
{
towar.Nazwa = "Nowa nazwa";
var cena = towar.Ceny["Hurtowa"];
cena.Netto = new DoubleCy(100.00m);
transaction.Commit(); // Zatwierdza zmiany w sesji
}
// Brak Commit() = rollback (przywrócenie stanu sprzed Logout)
session.Save(); // Zapisuje do bazy danych
}
```
### Logout(editMode: false) - transakcja tylko do odczytu
```csharp
using (var session = login.CreateSession(readOnly: false, config: false, name: "Przeglad"))
{
var tm = session.GetTowary();
var towar = tm.Towary.WgKodu["KOD001"];
// Transakcja tylko do odczytu
using (var readTrans = session.Logout(editMode: false))
{
// Tutaj NIE można modyfikować
// Ale można otworzyć zagnieżdżoną transakcję edycyjną
using (var editTrans = session.Logout(editMode: true))
{
var cena = towar.Ceny["Hurtowa"];
cena.Netto = new DoubleCy(200m);
editTrans.Commit();
}
// WAŻNE: Commit wymagany też dla transakcji readonly!
// Inaczej zmiany z zagnieżdżonych transakcji edycyjnych przepadną
readTrans.Commit();
}
session.Save();
}
```
| Metoda | Opis |
|--------|------|
| `session.Logout(editMode: true)` | Transakcja edycyjna - można modyfikować |
| `session.Logout(editMode: false)` | Transakcja tylko do odczytu |
| `transaction.Commit()` | Zatwierdza zmiany (wymagane dla obu typów!) |
| `transaction.CommitUI()` | Zatwierdza + odświeża UI |
| `transaction.Dispose()` | Bez Commit = rollback (także zagnieżdżonych zmian) |
## Kompletny przykład
```csharp
// 1. Pobranie instancji aplikacji
BusApplication app = BusApplication.Instance;
// 2. Pobranie bazy danych
Database db = app["MojaBaza"];
// 3. Logowanie
Login login = db.Login(new LoginParameters
{
UserName = "admin",
UserPassword = "haslo"
});
// 4. Utworzenie sesji (zawsze z nazwą!)
using (var session = login.CreateSession(readOnly: false, config: false, name: "Import"))
{
// 5. Pobranie modułu (extension method)
var tm = session.GetTowary();
// 6. Transakcja biznesowa - WYMAGANA dla zmian!
using (var transaction = session.Logout(editMode: true))
{
var nowyTowar = new Towar();
tm.Towary.AddRow(nowyTowar);
nowyTowar.Kod = "IMPORT001";
nowyTowar.Nazwa = "Zaimportowany towar";
transaction.Commit();
}
// 7. Zapis do bazy
session.Save();
}
// 8. Session automatycznie Dispose() przez using
```
## Thread-safety
### Obiekty multi-threaded (można współdzielić między wątkami)
- `BusApplication`
- `Database`
- `Login`
### Obiekty single-threaded (NIE współdzielić)
- `Session`, `Module`, `Table`, `Row`, `Context`
```csharp
// DOBRZE - Login można współdzielić, każdy wątek tworzy własną sesję
Login sharedLogin = db.Login(new LoginParameters { UserName = "admin", UserPassword = "haslo" });
Parallel.ForEach(items, item => {
using (var session = sharedLogin.CreateSession(readOnly: false, config: false, name: "Watek"))
{
var tm = session.GetTowary();
using (var transaction = session.Logout(editMode: true))
{
// operacje...
transaction.Commit();
}
session.Save();
}
});
```