20 KiB
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:
[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:
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
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<Params>();
var view = args.Session.GetHandel().Faktury.WgNumeru.CreateView();
view.AddExpression<Faktura>(f => f.Data >= pars.OdDaty && f.Data <= pars.DoDaty);
if (pars.Filtr == FiltrFaktur.TylkoNiezatwierdzone)
view.AddExpression<Faktura>(f => f.Stan == StanDokumentu.Bufor);
args.DataSource = view;
}
W InitContext ustawiasz Params w kontekście (args.Context.Set(...)), żeby:
- Pola filtru w
viewform.xml(EditValue="{FakturyViewInfo+Params.OdDaty}") mogły je odczytać. CreateViewmógł je odczytać przezargs.Context.GetRequired<Params>().
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.
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/LoadPropertyz kluczem zapisuje wartość w konfiguracji loginu (persystencja między sesjami).- Metody
GetList<X>()(opcjonalne) zwracają listę dozwolonych wartości (enum,View,LookupInfo.Item, tablica). - Metody
IsReadOnly<X>()/IsVisible<X>()(opcjonalne) sterują dostępnością/widocznością pola — bindowane zviewform.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ę:
// 1) AddExpression — najczytelniejszy, LINQ tłumaczony na SQL
view.AddExpression<Faktura>(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ćRowConditionbezpośrednio (Exists,Or, gotowy obiekt) oraz gdy filtrujesz po cechach (Features.X) — LINQ tego nie obsługuje.FilterCondition— tylko jako ostatnia deska ratunku. Działa po stronie klienta, nie da się przez nią paginować/sortować po SQL-u.
Filtrowanie po cechach (Features)
Cechy są dynamicznymi polami trzymanymi w osobnej tabeli — nie są typowanymi properties klasy Row. Dla cech używaj FieldCondition ze string-path "Features.NazwaCechy":
// Zwykłe pola Towaru — LINQ (preferowane, walidowane przy kompilacji)
view.AddExpression<Towar>(t => !t.Blokada && t.Typ == TypTowaru.Towar);
// Cechy — FieldCondition ze string-path (jedyna droga)
view.Condition &= new FieldCondition.Equal("Features.GrupaTowaru", "Telewizor");
view.Condition &= new FieldCondition.Equal("Features.Marka", "Samsung");
view.Condition &= new FieldCondition.GreaterEqual("Features.Przekatna", 50);
view.Condition &= new FieldCondition.LessEqual("Features.Przekatna", 75);
Path "Features.NazwaCechy" działa też w LikeConditionProvider, OrderBy widoku i bindingach viewform.xml (EditValue="{Features.Marka}"). Pełen opis cech (typy, składowanie, dostęp programowy) — patrz features.md.
Pełen opis RowCondition (rodzaje, Exists, Or/And, użycie w SubTable) — patrz rowcondition.md.
Powiązanie z viewform.xml
ResourceName → program wyszuka plik <ResourceName>.viewform.xml. Pola formularza bindują się do właściwości
ViewInfo i jego Params przez składnię {NazwaViewInfo+Params.Nazwa}:
<DataForm xmlns="...">
<Flow Name="FilterPanel">
<Field EditValue="{FakturyViewInfo+Params.OdDaty}"
CaptionHtml="Od daty"
Width="20"/>
<Field EditValue="{FakturyViewInfo+Params.Filtr}"
CaptionHtml="Filtr"
Width="20"/>
</Flow>
<Grid Name="List" OrderBy="Data">
<Field EditValue="{Numer}" CaptionHtml="Numer" Width="20"/>
<Field EditValue="{Data}" CaptionHtml="Data" Width="12"/>
<Field EditValue="{Kontrahent}" CaptionHtml="Kontrahent" Width="35"/>
<Field EditValue="{WartoscBrutto}" CaptionHtml="Brutto" Width="13" Footer="Sum"/>
</Grid>
</DataForm>
Reguły bindowania:
EditValue="{NazwaProperty}"w Grid — pole z aktualnego wiersza.EditValue="{NazwaViewInfo+Params.NazwaProperty}"w FilterPanel — właściwość zParams(klasa zagnieżdżona).Visibility="{NazwaViewInfo+Params.IsVisibleXxx()}",IsReadOnly="{NazwaViewInfo+Params.IsReadOnlyXxx()}"— wywołanie metody (z()).AppearancezCondition="{?[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#:
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<Params>();
var view = args.Session.GetHandel().Faktury.WgNumeru.CreateView();
view.AddExpression<Faktura>(f => f.Data >= pars.OdDaty && f.Data <= pars.DoDaty);
if (pars.Filtr == FiltrFaktur.TylkoNiezatwierdzone)
view.AddExpression<Faktura>(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:
<DataForm xmlns="...">
<Flow Name="FilterPanel">
<Field EditValue="{FakturyViewInfo+Params.OdDaty}" CaptionHtml="Od" Width="15"/>
<Field EditValue="{FakturyViewInfo+Params.DoDaty}" CaptionHtml="Do" Width="15"/>
<Field EditValue="{FakturyViewInfo+Params.Filtr}" CaptionHtml="Filtr" Width="25"/>
</Flow>
<Grid Name="List" OrderBy="Numer">
<Field EditValue="{Numer}" CaptionHtml="Numer" Width="20"/>
<Field EditValue="{Data}" CaptionHtml="Data" Width="12"/>
<Field EditValue="{Kontrahent}" CaptionHtml="Kontrahent" Width="35"/>
<Field EditValue="{WartoscBrutto}" CaptionHtml="Brutto" Width="13" Footer="Sum"/>
</Grid>
</DataForm>
Pułapki i dobre praktyki
AllowNew/AllowUpdate/AllowRemoveustawiaj wCreateView, nie w konstruktorze — operują na świeżymViewtworzonym przy każdym wejściu do folderu.InitContextmusi poprzedzaćCreateVieww łańcuchu zależności: wInitContextwsadzaszParamsdo kontekstu, wCreateViewje odczytujesz przezargs.Context.GetRequired<Params>(). 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/LoadPropertyużywają klucza per-ViewInfo ("CRM.Kontrahenci","Handel.Faktury"). Trzymaj go w stałej, żeby zmiana nazwy nie skasowała zapisanych preferencji.staticpola jako pamięć ostatniej wartości między sesjami — tylko gdy świadomie godzisz się na współdzielenie między równoległymi loginami (BusApplicationjest multithreaded — patrz "Thread-safety" w głównymSKILL.md). Domyślnie preferujSaveProperty/LoadProperty.- Nazewnictwo: nazwa klasy
<NazwaTabeli>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łównymSKILL.md. FilterConditionto ostatnia deska ratunku — działa po wczytaniu wierszy, ogranicza wydajność. Jeśli się da, przepisz naAddExpressionlubRowCondition.Exists.- Kod biznesowy vs UI: handlery
CreateView/InitContextto UI. Nie wołaj z nich workera biznesowego, jeśli ten ma działać poza interfejsem — wydziel logikę do zwykłej metody operującej naSubTable/Session.