From 97c478cc5163dcbfb3d005141dc2d3c4980a9684 Mon Sep 17 00:00:00 2001 From: Marcin Wojas Date: Mon, 18 May 2026 15:44:17 +0200 Subject: [PATCH] viewinfo.md --- soneta-programming/SKILL.md | 3 + soneta-programming/references/viewinfo.md | 382 ++++++++++++++++++++++ 2 files changed, 385 insertions(+) create mode 100644 soneta-programming/references/viewinfo.md diff --git a/soneta-programming/SKILL.md b/soneta-programming/SKILL.md index 4ac0eeb..3ad26ba 100644 --- a/soneta-programming/SKILL.md +++ b/soneta-programming/SKILL.md @@ -29,6 +29,7 @@ SKILL.md zawiera "duży obraz" - hierarchię klas, thread-safety, kanoniczne wzo | 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) | | RowCondition - serwerowe warunki LINQ, filtrowanie SubTable / View / Query | [references/rowcondition.md](references/rowcondition.md) | +| ViewInfo - definicja widoków list (folderów), CreateView, klasa Params, powiązanie z viewform.xml | [references/viewinfo.md](references/viewinfo.md) | | Gotowe wzorce kodu end-to-end (import, CRUD, obsługa błędów) | [references/examples.md](references/examples.md) | ## Architektura warstw @@ -235,6 +236,8 @@ Kod UI to np.: - 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) {...}`). +Szczegóły konstruowania ViewInfo (atrybut `FolderView`, eventy `CreateView`/`InitContext`, klasa `Params`, powiązanie z `viewform.xml`) opisuje [references/viewinfo.md](references/viewinfo.md). + ## Metadane modułów, tabel, kluczy, pól Dostęp do metadanych obiektów biznesowych jest dostępny przez metody `static` klasy `ApplicationInfo`. diff --git a/soneta-programming/references/viewinfo.md b/soneta-programming/references/viewinfo.md new file mode 100644 index 0000000..3d07ff4 --- /dev/null +++ b/soneta-programming/references/viewinfo.md @@ -0,0 +1,382 @@ +# ViewInfo — definicja widoku listy (folder) + +`ViewInfo` to klasa konfiguracyjna folderu/listy widocznego w drzewku Soneta. Definiuje co i jak jest w nim wyświetlane: który zasób UI (`viewform.xml`) załadować, jaką tabelę i z jakim filtrem otworzyć, jakie parametry filtru udostępnić użytkownikowi oraz jakie reguły dostępu/widoczności zastosować. + +ViewInfo to **kod UI** (patrz "Kod biznesowy vs UI" w głównym `SKILL.md`). + +Skill `soneta-form-xml` opisuje pełną składnię plików `viewform.xml`/`pageform.xml` (elementy `DataForm`, `Flow`, +`Grid`, `Field`, `Appearance`, `GroupBy`, atrybuty `EditValue`, `Visibility`, `IsReadOnly`, `Condition` itd.). Sięgaj do niego za każdym razem, gdy edytujesz lub generujesz XML formularza — bez tej wiedzy łatwo wygenerować nieprawidłowe znaczniki. + +--- + +## Rejestracja folderu — atrybut `FolderView` + +Klasa `ViewInfo` jest podpinana do drzewka folderów przez atrybut assemblowy `[assembly: FolderView(...)]`. To +jedyne miejsce, gdzie deklarujesz ścieżkę w menu, ikonę i typ ViewInfo: + +```csharp +[assembly: FolderView("CRM/Kontrahenci", + TableName = "Kontrahent", + Priority = 10, + ViewType = typeof(Soneta.CRM.UI.KontrahenciViewInfo), + Description = "Kartoteka kontrahentów z danymi handlowymi i kontaktowymi.", + IconName = "dom_osoba")] +``` + +Najważniejsze parametry: + +| Parametr | Znaczenie | +|---|---| +| `path` (pierwszy arg) | Ścieżka w drzewku — segmenty oddzielone `/`. Liście są folderami klikalnymi. | +| `TableName` | Nazwa tabeli ORM, do której domyślnie odnosi się folder. | +| `ViewType` | Typ klasy `ViewInfo` ładowanej dla tego folderu. | +| `Priority` | Kolejność w obrębie rodzica (niższe wyświetlane wcześniej). | +| `GroupIndex` | Grupa wizualna w obrębie poziomu. | +| `IconName` | Nazwa zasobu ikony. | +| `Description` | Tooltip / opis folderu. | + +Najważniejsze foldery `path` programu Soneta można znaleźć w skill /soneta-mcp-ui/common-folders.md. + +--- + +## Anatomia klasy ViewInfo + +ViewInfo można używać do definiowania: + +* widoku danych w folderach programu +* list na formularzach, gdy lista ma rozbudowaną logikę obsługi i filtrowania + +ViewInfo to `System.ComponentModel.Component` — działa we wzorcu inicjalizacji w konstruktorze. Minimalny szkielet: + +```csharp +public class FakturyViewInfo : ViewInfo { + + public FakturyViewInfo() { + ResourceName = "Faktury"; + AllowNewInPlace = false; + + CreateView += FakturyViewInfo_CreateView; + InitContext += FakturyViewInfo_InitContext; + } + + // ... handlery eventów, klasa Params, metody pomocnicze ... +} +``` + +Właściwości najczęściej ustawiane w konstruktorze: + +| Property | Znaczenie | +|---|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| `ResourceName` | Identyfikator zasobu UI — z niego wyliczana jest nazwa pliku `*.viewform.xml`. Domyślnie używana jest nazwa tabeli z `FolderView`. Można podać kilka zasobów oddzielonych przecinkami (próbowane po kolei). | +| `NewRowTable` | Nazwa tabeli, do której trafiają nowo dodawane wiersze (komenda *Dodaj*). | +| `NewRows` | Tablica `NewRowAttribute[]` — wiele wariantów nowego wiersza (rozwijane menu *Dodaj*). | +| `AllowNewInPlace` | `true` (domyślnie) — pozwala dodawać wiersze wprost w gridzie, w przypadku użycia ViewInfo na formularzach. Ustaw `false`, gdy wymagane jest otwarcie formularza. | +| `ReadOnly` | Wymusza `view.AllowNew = view.AllowRemove = false` w `CreateView`. Używane tylko na ViewInfo na formularzach. | +| `GhostField` | Nazwa "kolumny widmowej" — wykorzysytywane gdy lista zawiera inne obiekty niż otwierane na formularzu z listy. | +| `OpeningPageName` | Nazwa pliku `*.pageform.xml` otwieranego po kliknięciu wiersza (zastępuje przestarzałe `OpeningPageType`). | +| `MakeConfigSession` | `ThreeStateBoolean.Default/Tak/Nie` — czy sesja folderu ma być konfiguracyjna. Używane tylko na ViewInfo na formularzach. | + +`ResourceName = "Faktury"` → program szuka pliku `Faktury.viewform.xml` w lokalizacjach zasobów modułu. + +--- + +## Eventy — gdzie zaszywać logikę + +Logikę ViewInfo wprowadzasz przez handlery eventów dziedziczonych z `Soneta.Business.ViewInfo`. Podpinaj je w konstruktorze. Najczęściej używane: + +| Event | Sygnatura argumentów | Po co | +|---|---|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| `CreateView` | `CreateViewEventArgs` (`View`, `DataSource`, `Session`, `Context`) | Budowa głównego `View`: filtry, `Condition`, `AddExpression`, `FilterCondition`, `AllowNew/Update/Remove`. | +| `InitContext` | `ContextEventArgs` (`Context`, `FolderView`) | Wstrzyknięcie do kontekstu obiektu `Params`/`WParams` lub innych oraz helperów. Wywoływane **przed** `CreateView`. | +| `InitAllRowsContext` | jw. | Wariant "Wszystkie wiersze" — wyzeruj filtry do wartości "Razem"/"Wszystkie". Używany do mechanizmów globalnego wyszukiwania. Wtedy zbudowany View użyty jest przez "Global Search" programu i musi zwrócić wszystkie dostępne obiekty. | +| `UpdateDataSource` | `UpdateDataSourceEventArgs` | Reakcja na zmianę źródła. Rzadko nadpisywane. | +| `Action` | `ActionEventArgs` | Obsługa akcji wywołanej z toolbara/grida "Dodaj", "Edytuj", "Aktualizuj", "Usuń". | +| `CanDeleteRow` | `CanDeleteRowEventArgs` | Walidacja przed kasowaniem (zwrócenie błędu blokuje delete). | +| `SettingFocusedData` | `SettingFocusedDataEventArgs` | Ustalający wiersz w liście odpowiadający obiektowi na formularzu (może być inny). | + +### Kanoniczna para `InitContext` + `CreateView` + +```csharp +public FakturyViewInfo() { + NewRowTable = "Faktura".TranslateIgnore(); + ResourceName = "Faktury"; + + InitContext += FakturyViewInfo_InitContext; + CreateView += FakturyViewInfo_CreateView; +} + +private void FakturyViewInfo_InitContext(object sender, ContextEventArgs args) { + args.Context.Set(new Params(args.Context)); +} + +private void FakturyViewInfo_CreateView(object sender, CreateViewEventArgs args) { + var pars = args.Context.GetRequired(); + + var view = args.Session.GetHandel().Faktury.WgNumeru.CreateView(); + view.AddExpression(f => f.Data >= pars.OdDaty && f.Data <= pars.DoDaty); + + if (pars.Filtr == FiltrFaktur.TylkoNiezatwierdzone) + view.AddExpression(f => f.Stan == StanDokumentu.Bufor); + + args.DataSource = view; +} +``` + +W `InitContext` ustawiasz Params **w kontekście** (`args.Context.Set(...)`), żeby: +1. Pola filtru w `viewform.xml` (`EditValue="{FakturyViewInfo+Params.OdDaty}"`) mogły je odczytać. +2. `CreateView` mógł je odczytać przez `args.Context.GetRequired()`. + +--- + +## Klasa parametrów (`Params` / `WParams` / lub inna nazwa) + +`Params` to klasa publiczna (może być zagnieżdżona, ale trzeba uważać na identyfikator) dziedzicząca z `ContextBase` +(lub z `Params` innego ViewInfo). +Trzyma stan filtrów widoku — wartości pól z `FilterPanel`. Szczegóły dotyczące `ContextBase`, `InvokeChanged`, +persistencji i kontekstu — patrz [contextbase.md](contextbase.md). + +```csharp +public class Params : ContextBase { + + protected string key = "Handel.Faktury"; + + public Params(Context context) : base(context) { + Load(); + } + + public void Load() { + filtr = LoadProperty(nameof(Filtr), key, FiltrFaktur.Wszystkie); + osDaty = LoadProperty(nameof(OdDaty), key, Date.Today.GetFirstOfMonth()); + } + + private FiltrFaktur filtr; + + [Caption("Filtr")] + public FiltrFaktur Filtr { + get => filtr; + set { + filtr = value; + Session.InvokeChanged(); + SaveProperty(nameof(Filtr), key); + } + } + + public object GetListFiltr() => Enum.GetValues(typeof(FiltrFaktur)); + + private Date odDaty; + + [Caption("Od daty")] + public Date OdDaty { + get => odDaty; + set { + odDaty = value; + Session.InvokeChanged(); + SaveProperty(nameof(OdDaty), key); + } + } + + public bool IsReadOnlyOdDaty() => filtr == FiltrFaktur.Wszystkie; +} +``` + +Konwencje: +- Każda property z `[Caption]` (lokalizowalna nazwa pola w UI). +- Setter wywołuje `Session.InvokeChanged()` — wymusza refresh View. +- `SaveProperty/LoadProperty` z kluczem zapisuje wartość w konfiguracji loginu (persystencja między sesjami). +- Metody `GetList()` (opcjonalne) zwracają listę dozwolonych wartości (enum, `View`, `LookupInfo.Item`, tablica). +- Metody `IsReadOnly()` / `IsVisible()` (opcjonalne) sterują dostępnością/widocznością pola — bindowane z `viewform.xml`. + +**Współdzielenie state przez Context**: jeśli kilka folderów ma operować na tej samej dacie / liście płac, można trzymać wartość w `Context[typeof(Date)]` / `Context[typeof(ListaPlac)]` zamiast w polu klasy — wtedy wszystkie ViewInfo widzące ten sam Context dzielą wartość. + +--- + +## Filtrowanie View — paleta narzędzi + +W handlerze `CreateView` masz do dyspozycji kilka mechanizmów. Wybieraj najprostszy, który załatwia sprawę: + +```csharp +// 1) AddExpression — najczytelniejszy, LINQ tłumaczony na SQL +view.AddExpression(f => f.Data >= pars.OdDaty && f.Status == StatusDok.Zatwierdzony); + +// 2) Condition + FieldCondition — proste porównania +view.Condition &= new FieldCondition.Equal("Magazyn", pars.Magazyn); + +// 3) RowCondition.Exists — EXISTS subquery (relacje 1:N filtrowane przez dziecko) +view.Condition &= new RowCondition.Exists( + "OsobyKontrahenci", "Kontrahent", + new FieldCondition.Equal("OsobaKontaktowa", pars.Osoba)); + +// 4) FilterCondition — walidacja per-wiersz (gdy logika nie da się zSQL-ować, np. uprawnienia) +view.FilterCondition += (s, e) => { + if (e.Row is Faktura f) + e.Accepted &= f.GetObjectRight() != AccessRights.Denied; +}; + +// 5) Zablokowanie operacji - lista na formularzu +if (pars.TylkoPodgląd) { + view.AllowNew = false; + view.AllowUpdate = false; + view.AllowRemove = false; +} +``` + +Reguły wyboru: +- **`AddExpression`** — gdy filtr da się wyrazić jako LINQ na właściwościach kolumn. Preferowany. +- **`Condition &=`** — gdy potrzebujesz dołożyć `RowCondition` bezpośrednio (`Exists`, `Or`, gotowy obiekt). +- **`FilterCondition`** — tylko jako ostatnia deska ratunku. Działa po stronie klienta, nie da się przez nią paginować/sortować po SQL-u. + +Pełen opis `RowCondition` (rodzaje, `Exists`, `Or`/`And`, użycie w `SubTable`) — patrz [rowcondition.md](rowcondition.md). + +--- + +## Powiązanie z `viewform.xml` + +`ResourceName` → program wyszuka plik `.viewform.xml`. Pola formularza bindują się do właściwości +ViewInfo i jego `Params` przez składnię `{NazwaViewInfo+Params.Nazwa}`: + +```xml + + + + + + + + + + + + + +``` + +Reguły bindowania: +- `EditValue="{NazwaProperty}"` w Grid — pole z aktualnego wiersza. +- `EditValue="{NazwaViewInfo+Params.NazwaProperty}"` w FilterPanel — właściwość z `Params` (klasa zagnieżdżona). +- `Visibility="{NazwaViewInfo+Params.IsVisibleXxx()}"`, `IsReadOnly="{NazwaViewInfo+Params.IsReadOnlyXxx()}"` — wywołanie metody (z `()`). +- `Appearance` z `Condition="{?[FlagaBlokady] = True}"` — formatowanie warunkowe wiersza. +- `Footer="Sum"` - kolumny liczbowe mogą mieć sumę w stopce. + +Pełna gramatyka `viewform.xml` (DataForm, Page, Group, Grid, Field, Stack, Flow, Command, Appearance, GroupBy, Renderable, CaptionHtml, Footer, Class) — używaj skilla **`/soneta-form-xml`**. Bez niego wygenerowany XML łatwo zawiera nieistniejące elementy. + +--- + +## Pełny przykład minimalny + +Klasa C#: + +```csharp +using System; +using Soneta.Business; +using Soneta.Business.UI; +using Soneta.Handel; +using Soneta.Types; + +[assembly: FolderView("Handel/Faktury własne", TableName = "Faktura", + Priority = 50, GroupIndex = 1, IconName = "dokument_wy", + ViewType = typeof(Soneta.Handel.UI.FakturyViewInfo))] + +namespace Soneta.Handel.UI; + +public class FakturyViewInfo : ViewInfo { + + public FakturyViewInfo() { + NewRowTable = "Faktura".TranslateIgnore(); + ResourceName = "Faktury"; + AllowNewInPlace = false; + + InitContext += FakturyViewInfo_InitContext; + CreateView += FakturyViewInfo_CreateView; + } + + private void FakturyViewInfo_InitContext(object sender, ContextEventArgs args) { + args.Context.Set(new Params(args.Context)); + } + + private void FakturyViewInfo_CreateView(object sender, CreateViewEventArgs args) { + var pars = args.Context.GetRequired(); + + var view = args.Session.GetHandel().Faktury.WgNumeru.CreateView(); + view.AddExpression(f => f.Data >= pars.OdDaty && f.Data <= pars.DoDaty); + if (pars.Filtr == FiltrFaktur.TylkoNiezatwierdzone) + view.AddExpression(f => f.Stan == StanDokumentu.Bufor); + + args.DataSource = view; + } + + public enum FiltrFaktur { Wszystkie, TylkoZatwierdzone, TylkoNiezatwierdzone } + + public class Params : ContextBase { + const string key = "Handel.Faktury"; + + public Params(Context context) : base(context) { + odDaty = LoadProperty(nameof(OdDaty), key, Date.Today.GetFirstOfMonth()); + doDaty = LoadProperty(nameof(DoDaty), key, Date.Today); + filtr = LoadProperty(nameof(Filtr), key, FiltrFaktur.Wszystkie); + } + + private Date odDaty; + + [Caption("Od daty")] + public Date OdDaty { + get => odDaty; + set { odDaty = value; Session.InvokeChanged(); SaveProperty(nameof(OdDaty), key); } + } + + private Date doDaty; + + [Caption("Do daty")] + public Date DoDaty { + get => doDaty; + set { doDaty = value; Session.InvokeChanged(); SaveProperty(nameof(DoDaty), key); } + } + + private FiltrFaktur filtr; + + [Caption("Filtr")] + public FiltrFaktur Filtr { + get => filtr; + set { filtr = value; Session.InvokeChanged(); SaveProperty(nameof(Filtr), key); } + } + } +} +``` + +Towarzyszący `Faktury.viewform.xml`: + +```xml + + + + + + + + + + + + + + +``` + +--- + +## Pułapki i dobre praktyki + +- **`AllowNew`/`AllowUpdate`/`AllowRemove`** ustawiaj w `CreateView`, nie w konstruktorze — operują na świeżym `View` tworzonym przy każdym wejściu do folderu. +- **`InitContext` musi poprzedzać `CreateView`** w łańcuchu zależności: w `InitContext` wsadzasz `Params` do + kontekstu, w `CreateView` je odczytujesz przez `args.Context.GetRequired()`. Nie próbuj odwrotnej kolejności. +- **`Session.InvokeChanged()` w setterach Params** jest obowiązkowe — bez niego zmiana pola filtru nie odświeży listy. +- **`SaveProperty`/`LoadProperty`** używają klucza per-ViewInfo (`"CRM.Kontrahenci"`, `"Handel.Faktury"`). Trzymaj go w stałej, żeby zmiana nazwy nie skasowała zapisanych preferencji. +- **`static` pola jako pamięć ostatniej wartości** między sesjami — tylko gdy świadomie godzisz się na współdzielenie między równoległymi loginami (`BusApplication` jest multithreaded — patrz "Thread-safety" w głównym `SKILL.md`). Domyślnie preferuj `SaveProperty`/`LoadProperty`. +- **Nazewnictwo**: nazwa klasy `ViewInfo` (l.mn. dla list — `KontrahenciViewInfo`, `FakturyViewInfo`), + namespace zgodny z modułem (`Soneta.Handel.UI`). Identyfikatory domenowe po polsku, systemowe po angielsku — patrz + "Konwencje nazewnicze" w głównym `SKILL.md`. +- **`FilterCondition` to ostatnia deska ratunku** — działa po wczytaniu wierszy, ogranicza wydajność. Jeśli się da, przepisz na `AddExpression` lub `RowCondition.Exists`. +- **Kod biznesowy vs UI**: handlery `CreateView`/`InitContext` to UI. Nie wołaj z nich workera biznesowego, jeśli ten ma działać poza interfejsem — wydziel logikę do zwykłej metody operującej na `SubTable`/`Session`.