WebExpress 2.0.0‑alpha – View, State and Service, Part II: From Pro‑Control State to ViewStated Contexts
An update to the earlier post about the View, State, and Service architecture.
When the View, State, and Service architecture was introduced, it replaced scattered state, direct DOM manipulation, and implicit service calls with a clear unidirectional data flow. Each control gained an explicit state object, a typed service descriptor, and a predictable path from initialization through user interaction to re‑rendering. The model worked well and was written entirely in C#. The previous post showed how a list evolved from a handful of scattered properties into a single, well‑structured definition. As the architecture rolled out across control families, a deeper boundary became visible. The state was clean but still belonged per control. Each control seeded its own state island, resolved its own service, and loaded itself on mount. Two controls on the same screen showing the same data fetched it independently. There was no natural way for one control to trigger a re‑render in another when shared data changed, nor could a control ask the page to re‑execute a central query. Authoring still relied on strings: a resource or service was named by a string resolved at runtime, typos the compiler could never catch. And beneath the component, the engine still carried a separate store layer. This update describes the next step and why it was taken. The motivation is the same as before, just one level deeper: to make client behavior clearer, more predictable, and more reusable, while keeping every definition in C#. The result is a model where state lives per ViewState, resources are loaded centrally, controls automatically re‑render when shared state changes, any control can trigger a re‑query, and the entire authoring surface is type‑safe. State belongs per ViewState, not per control. A ViewState is a region of the page. The page itself is simply the outermost ViewState, and ViewStates can nest. A ViewState is declared once and owns the state, services, and resources of that region. Controls no longer carry private stores. (The word scope now means something different, it is the placement concept for fragments, [Scope<T>], so the region that owns shared data is deliberately called a ViewState, never a scope.) Resources are loaded centrally. A resource is a named, central query. The ViewState loads it once on mount and reduces the result into a slice of the ViewState state. Two controls bound to the same resource share that single load operation. Controls become pure views. A control binds to a resource, subscribes to its slice, and re‑renders whenever the ViewState triggers a re‑query. Any control can trigger that re‑query, and all controls bound to the resource re‑render from the result. The view still produces no input and no output of its own. Writing surfaces close the loop. A pure view reads a resource; a writing surface changes it. A quickfilter, a search or WQL prompt, a form, or a comment composer binds to the same resource with control.Resource<TResource>().Model(...), writes its value into the shared ViewState state, and triggers the central re‑query, so every control bound to that resource re‑renders from the result. This is the write counterpart to the read binding: the same declarative, type‑safe surface, one direction for reading and one for writing. Because the surface writes the state the resource reads, the older control‑to‑control wiring that pushed a search or filter from one control into another is no longer needed inside a ViewState. The store is gone. The ViewState is now the only observable state primitive. There's no separate store to wrap, and exactly one place holds the truth for a region of the page. Authoring is type‑safe. Services are referenced via Service<TEndpoint>(), resources via Resource<TResource>(), and a control binds with control.Resource<TResource>(). No resource or service name is ever written as a string. The names carried by the islands on the wire are derived from types. A typo is now a compiler error instead of a silent runtime deviation. The ViewState holds only state, services, and resources (never controls). Controls are created separately and bound to the ViewState by type. Since a control resolves its ViewState through the resource it binds to, not its DOM position, ViewStates can serve controls anywhere on the page, even fragments mounted in different sections. A fragment form of the ViewState (FragmentControlViewState) completes the picture, allowing a ViewState to contribute to a page like any other fragment. The previous post ended with this definition authoring a control that owned its own state and service: new ControlDataList("myList") .State(s => s .Page(0) .PageSize(25)) .DataService<MonkeyIslandGamesList>(); The same view is now authored as a ViewState plus pure view. The list is created separately and bound by type to a resource. The ViewState declares state, service, and resource, all referenced by type instead of string: // the list is a pure view, bound by type to the Games resource var list = new ControlDataList("myListView").Resource<GamesResource>(); // the ViewState owns state, service, and the central resource new ControlViewState<DataQueryState>("games") .State(s => { s.Page = 0; s.PageSize = 25; }) .Service<MonkeyIslandGamesList>(svc => svc .Method(HttpMethod.Get) .Query(q => q.Search().Wql().Filter().Page().PageSize().OrderBy().OrderDir()) .Response(r => r.Items().Total())) .Resource<GamesResource>(r => r .Service<MonkeyIslandGamesList>() .Page().PageSize().Search().Wql().Filter().OrderBy().OrderDir()); The difference is conceptual rather than syntactic. The earlier line described a control that happened to load data. The new definition describes a ViewState and a view of it. If a second control binds to the same GamesResource, both update from a single load whenever a bound search box writes the shared state. The data flow made explicit within one control is now explicit across an entire region of the page. The C#‑first principle remains unchanged: state, services, resources, bindings, and actions are still declared entirely in C#, with JavaScript remaining only the engine that executes them. What shifts is merely where a few things are declared. State and Service move from the control into the ViewState. The control keeps its own UI configuration and receives a single typed resource binding, control.Resource<TResource>(). This binding is fluent and retains the concrete control type. Resource and service identities are now types: a resource is a small marker type, and a service is its endpoint type. The default query state has a typed model (DataQueryState), so seeding occurs through typed properties instead of string keys. The per‑control surface DataService<TEndpoint>() remains for truly self‑contained controls that aren't part of a shared region, such as a single graph editor that loads a document and autosaves independently. Forcing such a control into a ViewState would add machinery without benefit. For data families sharing a region, the ViewState is home to state, services, and resources. The qualities delivered by the first step are carried forward and improved. The model stays predictable because only the ViewState writes state and only the view produces DOM. It remains testable, and the central resource provides a single place to verify how a query maps to a request and how a response reduces into state. It stays compatible because the markup contract and registration model remain intact. And it's now safer because references that used to be strings are now types checked by the compiler. This step continues the direction set by the architecture: a clear foundation that scales from the simplest control to the most demanding application, fully authored in C#. Many of the concepts introduced here are intentionally open, meant to evolve in real projects and be refined through practical feedback. Anyone wishing to contribute is warmly invited to share ideas, suggestions, issues, or extensions. Every contribution helps sharpen the model and advance WebExpress as a modern, robust, and community‑driven framework.

Comments
Post a Comment