Jump to content

Optimising Screen Updates and User Experience in the GenericEdit Framework


Recommended Posts

ginjaninja
Posted (edited)

My minimal understanding is start to butt up against achieving a nice UI experience and i was hoping people could part the clouds of understanding a little and establish  some relevant ground truths (for me to direct my efforts and learning).if you would be kind enough to spare some time.


This is an AIs understanding of the Emby genericedit framework (im probably not using the right term..maybe genericUI?PluginUI?). Would people support the key bottom line points / disagree with any of it?
The main issue i want to get an understanding of is; how much 'liveness' is realistic to try and achieve in the UI whilst maintaining a reasonable user experience. Are the suggested patterns good ones to adopt?

'Issues' i am hitting

  • Being returned to the top of the page when clicking on checkbox in a dxgrid.
  • Rows in genericitemlist members bouncing in and out of view whilst a task which is working on them and moving them to 'completed' is processing them.
  • Different UI states when nothing is happening and i navigate away and backagain (Understanding the lifetime of data in the framework perhaps)


 I am talking about plugins with what i assume are  trivially small 'UI data sets'. I want the UI to be responsive, live and survive page navigations back to main emby and back to plugin but i wonder if my expectations are unrealistic and where I should focus efforts.

[deleted content for being rubbish/not true]

 

I have no  training in development. i am learning by osmosis, the reference docs where they can help, and the forum. I am only a beginner understanding c# (but ai can help with that), i think UI is a few steps beyond that, so be gentle 🙂.
Thanks in anticipation.

Edited by ginjaninja
Posted

@ginjaninja

Holy cow - how can I get this incredible bunch of nonsense out of your head...?

Should I dissect all the false statements or start fresh - I'm not sure. Almost every sentence is wrong.

First of all, terms disambiguation - and why there are so many different terms:

  • GenericEdit
    This provides the definition of an editable component which gets presented on some surface in an interactive way
    (we have cases where GenericEdit is used all alone, without GenericUI - for example in the Win,Xbox and Linux apps)
  • GenericUI
    Provides the framing around a GenericEdit component as a page, dialog or wizard and the APIs for communicating with the server (and back)
    (we have built-in cases where GenericUI is used but without PluginUI)
  • PluginUI
    Is a layer - or primarily a set of base classes intended to make it easy to use GenericUI + GenericEdit from a plugin

Here are some general points which are crucial to understand:

  • GenericEdit/UI is agnostic of UI platform and technology
    It has nothing to do with HTML or DOM in the first place. It's possible (for us) at any time, to to create a client-side implementation for a different technology, like for example Android or iOS native UI. At some point, I had started an implementation for WinForms (just for fun - I didn't get very far due to time constraints) - thats absolutely doable without any changes on the side of plugin developers 
  • State
    State is global. Each plugin UI view has exactly one state: The values at the server
  • Refresh
    Is up to you. You can raise the UI view change event from the server and then, all clients currently viewing that UI view will see the updated view.
    Sometimes, this is desired and sometimes not: for example, when the user is supposed to set multiple parameters and finally save them via button click. In that case, it can happen that two clients are showing different values - when at least one of them hasn't submitted their pending changes
    You can decide whether you either want to have a "last one wins" behavior, or send a changed event when one client submits an update. This will throw away potential changes which somebody else (or the same user) might have made - but not submitted

For the issues you mentioned:

  • Being returned to the top of the page when clicking on checkbox in a dxgrid.
  • Rows in genericitemlist members bouncing in and out of view
  •  

...we'd need to look at the specific cases.

 

Posted
1 hour ago, softworkz said:

Holy cow - how can I get this incredible bunch of nonsense out of your head...?

Kinda pointless to argue with an AI-generated Post?

Posted
4 minutes ago, TMCsw said:

Kinda pointless to argue with an AI-generated Post?

The post is not AI generated - I know he is working with these things and his questions are very legitimate.

He just - then - quoted what an AI model thinks about it, and the one thing I would argue is that this specific model is not among the brightest candles on the tree..

ginjaninja
Posted
On 26/06/2026 at 02:05, softworkz said:

The post is not AI generated - I know he is working with these things and his questions are very legitimate.

He just - then - quoted what an AI model thinks about it, and the one thing I would argue is that this specific model is not among the brightest candles on the tree..

Thanks @softworkzfor your time, apologies for using an ai to complement my thoughts / understanding; i am trying to understand why when i tell a genericitemlist to have a certain icon or status or button status, the end result of what the user sees, does not feel deterministic (despite the server doing what it needs to do perfectly) eg a row which is NEVER told to have a status of Inprogress in any of the code, ends up having an inprogress status in the client UI (a spinning circle in other words); noise from a nearby row (which was in progress). (I am using UIBaseClasses from the SDK demo).

So at the risk of outstaying my welcome, ive had another bite at the cherry at using an ai to aid understanding how to use a 'tracker' with locks as 'a single point of truth' to guarantee the client is showing the "real picture". To try and gain good will, i maintain it is not posted blindly it has been built up over my (admittedly naïve) observations/testing with ai support. Would you say any elements of this summary/understanding rings true or false or unnecessary.

 

🏛️ The Three-Layer Scope & Lifecycles
To achieve absolute UI robustness and flawless navigation hydration, your system separations must be mapped across three distinct, decoupled lifecycles.
Custom Framework Architecture & Scopes
┌────────────────────────────────────────────────────────────────────────┐
│  1. GLOBAL PERMANENT APPLICATION SCOPE                                 │
│  (Instantiated once at Emby startup. Lives 24/7 in system RAM)         │
│                                                                        │
│  ┌──────────────────────────────┐    ┌──────────────────────────────┐  │
│  │     Domain Task Services     │    │   Thread-Locked Trackers     │  │
│  │  • Executes business logic   ├───►│  • Single source of truth    │  │
│  │  •                           │    │  • Passive data storage ONLY │  │
│  └──────────────────────────────┘    └──────────────┬───────────────┘  │
└─────────────────────────────────────────────────────┼──────────────────┘
                                                      │ Injected on Page Request
                                                      ▼
┌────────────────────────────────────────────────────────────────────────┐
│  2. TRANSIENT RE-RENDERING ROUTER SCOPE                                │
│  (Short-lived. Created when user opens dashboard, killed on exit)      │
│                                                                        │
│  ┌──────────────────────────────────────────────────────────────────┐  │
│  │ ControllerBase : IPluginUIPageController                        │  │
│  │  • MainPageController : ControllerBase                           │  │
│  │  • TabPageController  : ControllerBase                           │  │
│  │                                                                  │  │
│  │  [The Core Factory Hook]                                         │  │
│  │  └──► Emby executes native: CreateDefaultPageView()              │  │
│  └────────────────────────────────┬─────────────────────────────────┘  │
└───────────────────────────────────┼────────────────────────────────────┘
                                    │ Lazily executes active tab lambda
                                    ▼
┌────────────────────────────────────────────────────────────────────────┐
│  3. STATELESS RENDER SCOPE                                             │
│  (Short-lived. Destroyed and fully reconstructed on every single push)│
│                                                                        │
│  ┌──────────────────────────────────────────────────────────────────┐  │
│  │ PluginPageView : PluginViewBase, IPluginUIView                   │  │
│  │  • AddMoviePageView : PluginPageView                             │  │
│  │                                                                  │  │
│  │  [The Component Data Model Hook]                                 │  │
│  │  ├──► Reads state snapshot, wipes rows, populates list           │  │
│  │  └──► Injects fresh layout: this.ContentData = new AddMovieUI(); │  │
│  └──────────────────────────────────────────────────────────────────┘  │
└────────────────────────────────────────────────────────────────────────┘

🔀 The Two Operational Pathways To State Change
Data mutations inside the trackers are driven by two entirely separate execution flows. Both channels alter state uniformly within a thread-safe synchronization gate.
Interaction vs Automation Processing Streams
[ PATHWAY A: USER INTERACTION ]             [ PATHWAY B: BACKGROUND AUTOMATION ]
 ┌───────────────────────────┐               ┌─────────────────────────────────┐
 │   User Clicks UI Button   │               │ Scheduled Task / Disk Poller    │
 └─────────────┬─────────────┘               └────────────────┬────────────────┘
               │                                              │
               ▼ Sends packed data string                     ▼ Triggers automatically
 ┌───────────────────────────┐                                │
 │  PluginPageView Subclass  │                                │
 │  • Overrides RunCommand() │                                │
 └─────────────┬─────────────┘                                │
               │                                              │
               ▼ Extracts IDs & dispatches                    │
 ┌───────────────────────────┐                                │
 │    Domain Task Service    │◄───────────────────────────────┘
 │ (Runs core business logic)│
 └─────────────┬─────────────┘
               │
               ▼ Updates data values via C# Thread Lock
 ┌───────────────────────────┐
 │   Thread-Locked Tracker   │
 └───────────────────────────┘
🛡️ The Double-Ended Synchronization Gate (Read/Write Locks)
Because your background domain services (running on asynchronous worker threads) and Emby's page-generation requests (running on web server UI threads) happen in parallel, a mutual exclusion fence must guard both entry and exit points.
Using the exact same memory traffic-light object (_stateLock), the tracker enforces an Atomic Transaction pattern to ensure data is never read or broadcast while it is fragmented.
       BACKGROUND WORKER THREAD                          WEB SERVER UI THREAD
    (Running Automated Operations)                   (Executing Emby Page Fetch)
 ┌──────────────────────────────────┐             ┌──────────────────────────────────┐
 │ 1. Opens transaction payload     │             │                                  │
 ├──────────────────────────────────┤             │                                  │
 │ 2. ACQUIRES WRITER LOCK          │             │                                  │
 │    (Traffic light turns RED)     │             │                                  │
 │                                  │             │                                  │
 │ 3. Writing fresh data, progress, │             │                                  │
 │    and modifying arrays...       │             │ 4. Client HTTP request hits the  │
 │                                  │             │    server router.                │
 │                                  │             ├──────────────────────────────────┤
 │                                  │             │ 5. Executes View Factory and     │
 │                                  │             │    calls: tracker.GetSnapshot()  │
 │                                  │             ├──────────────────────────────────┤
 │                                  │             │ 6. BLOCKED BY READER LOCK        │
 │                                  │             │    (Traffic light is RED!)       │
 │                                  │             │    Thread pauses dead at gate... │
 ├──────────────────────────────────┤             │                                  │
 │ 7. FINISHES TRANSACTION          │             │                                  │
 │    Releases lock. Light turns    │             │                                  │
 │    GREEN. Raises StateChanged    │             │                                  │
 └──────────────────────────────────┘             │                                  │
                                                  ▼ Wakes up instantly!
                                                  ┌──────────────────────────────────┐
                                                  │ 8. ACQUIRES READER LOCK          │
                                                  │    (Traffic light is GREEN)      │
                                                  │ 9. Safely copies complete,       │
                                                  │    "whole and complete" snapshot.│
                                                  │ 10. Releases lock & draws rows.  │
                                                  └──────────────────────────────────┘
  • The Writer Lock (The Commit): When a domain service needs to push updates, it packages the fields into a local, isolated temporary DTO bundle and commits them all at once inside a single lock (_stateLock) statement. The tracker never exposes properties mid-update.
  • The Reader Lock (The HTTP GET Interceptor): The HTTP GET network command itself doesn't know what a lock is. However, your view constructor invokes tracker.GetSnapshot(), which forces the web server's UI thread through a matching lock (_stateLock) barrier.
  • The Guard Reality: If the background worker thread is halfway through altering item status indicators, the UI thread handling the browser's fetch request is physically frozen at the gate processor line. It safely waits, preventing the generation of a corrupt or "torn" ContentData payload.

 


🆔 The Client ID Deficit & Element Reconciliation Rules
Emby's client engine does not maintain stable, persistent element IDs for structural containers like GenericListContainer or row items like GenericListItem. Because the client cannot reliably track if an item updates silently, shifts positions, or gets deleted, partial layout updates are completely impossible.
The Destructive Rebuild Strategy
                     [ Service Updates Tracker ]
                                 │
                                 ▼
                     [ Push Scheduler Evaluates ]
                                 │
                                 ▼ Passes 500ms Speed Gate?
           [ Target View Instance Fired: RaiseUIViewInfoChanged() ]
                                 │
                                 ▼ Client receives WebSocket, sends HTTP GET
 ┌───────────────────────────────────────────────────────────────────────┐
 │  CONTROLLER FACTORY LAYER (ControllerBase)                            │
 │                                                                       │
 │  • Emby executes: Task<IPluginUIView> CreateDefaultPageView();        │
 │  • Triggering your tab lambda closure to construct a fresh View.      │
 └───────────────────────────────┬───────────────────────────────────────┘
                                 │
                                 ▼ Completely pristine memory initialization
 ┌───────────────────────────────────────────────────────────────────────┐
 │  STATELESS VIEW FACTORY GENERATION STEP (PluginPageView)              │
 │                                                                       │
 │  1. Pulls immutable TrackerSnapshot                                   │
 │  2. Instantiates completely clean UI arrays and list elements         │
 │  3. Assigns brand-new data container: ContentData = new AddMovieUI()  |
 └───────────────────────────────┬───────────────────────────────────────┘
                                 │
                                 ▼ Returns entire layout payload as fresh JSON
 ┌───────────────────────────────────────────────────────────────────────┐
 │  CLIENT-SIDE DOM CLOBBERING                                           │
 │                                                                       │
 │  1. Client drops old layout fragment entirely due to missing IDs      │
 │  2. Draws brand-new list structure from scratch instantly             │
 └───────────────────────────────────────────────────────────────────────┘

🎛️ The User Action Interception Rules
Because elements are completely destroyed and re-drawn without stable client-side tracking IDs, user buttons must carry their own identification context to pass back to the server.
  • Where the Logic Sits: Interaction interception is completely stripped from the MainPageController. It runs natively inside your PluginPageView subclass by overriding the native SDK hook: public override Task<IPluginUIView> RunCommand(string itemId, string commandId, string data).
  • Self-Contained Parameter Packing: To bypass the client-side ID limitation, every row-level button encodes its target domain unique ID and list index directly into its payload properties (e.g., CommandId = "SELECT", Data1 = string.Format("Select_{0}_{1}", entry.Id, i)).
  • The Split Rule: The RunCommand method intercepts the postback string, splits it using token parameters (e.g., data.Split('_')), and cleanly forwards the separated context (Id and Index) to the long-lived business logic services, bypassing the controllers completely.

⏱️ The Client-Protection Gating Rule (Push Throttling)
Because your stateless architecture drops and completely clobbers the layout on every update, rapid sequential data updates (like progress bars) will cause severe UI lag on low-powered client hardware. The push scheduler enforces a rigid 2 Hz speed limit blindly.
  • The Safe Refresh Limit: Pushing updates must be restricted to a maximum of 2 times per second (2 Hz / 500ms minimum window) for fast-moving progress.
  • The Implementation: The system pipes all tracker mutations through a time-gated scheduler. If the data tracker changes at 50ms intervals, the scheduler updates the in-memory values silently but drops the UI push request until the 500ms throttle timer expires.
  • The Navigation Interceptor (Bypassing the Throttle): While progress ticks are throttled, direct user navigation (opening the page fresh, returning, or switching tabs) always bypasses the timer because Emby forces an immediate invocation of CreateDefaultPageView(), ensuring instant UI visibility.

🔄 The Network Refresh and Redraw Loop
When RaiseUIViewInfoChanged() fires blindly outside the tracker lock, it triggers a precise network and rendering chain reaction through the Emby core framework:
  [ UiPushScheduler ] ────► Calls activeView.RaiseUIViewInfoChanged()
                                            │
                                            ▼ Fires underlying native SDK event
┌────────────────────────────────────────────────────────────────────────┐
│ 1. EMBY SERVER CORE (WebSocket Gateway)                                 │
│    • Bundles a basic notify frame: {"Type": "UIViewInfoChanged", ...}   │
│    • Broadcasts this raw frame out to all connected WebSockets.         │
└───────────────────────────────────────────┬────────────────────────────┘
                                            │ WebSocket Frame sent over Network
                                            ▼
┌────────────────────────────────────────────────────────────────────────┐
│ 2. EMBY CLIENT APP (Chrome Browser )                                    │
│    • Receives the WebSocket frame.                                      │
│    • Checks: "Am I currently displaying this view context?"             │
│      ├───► (No)  --> DISCARDS packet. End of loop.                      │
│      └───► (Yes) --> Fires automatic HTTP GET back to server for layout.│
└───────────────────────────────────────────┬────────────────────────────┘
                                            │ HTTP GET /emby/UI/View?PageId=X
                                            ▼
┌────────────────────────────────────────────────────────────────────────┐
│ 3. EMBY SERVER ROUTER & CONTROLLERS (MainPageController Initialization)│
│    • Catches HTTP request. Instantiates MainPageController.            │
│    • Identifies which Tab was active via incoming URL variables.       │
│    • NATIVE SDK EXECUTION: Calls: CreateDefaultPageView()              │
└───────────────────────────────────────────┬────────────────────────────┘
                                            │ 
                                            ▼
┌────────────────────────────────────────────────────────────────────────┐
│ 4. STATELESS TAB VIEW (The Final Render)                                │
│    • View Constructor reads an immutable snapshot: tracker.GetSnapshot()│
│    • Completely wipes old row listings to avoid ghosting or misalignment.│
│    • Maps fresh snapshot variables into clean UI components.             │
│    • Sets content data property: ContentData = new AddMovieUI();        │
└───────────────────────────────────────────┬────────────────────────────┘
                                            │ HTTP Response returns JSON Layout
                                            ▼
 [ Emby Client App ] ────► Completely re-draws the Screen instantly.

🛡️ Core Robustness Rules (Why It Doesn't Break)
────────────────────────────────────────────────────────────────────────┘
  • Thread-Safe Locks (No Race Conditions): Mutating data fields inside the tracker uses a C# lock. Short-lived view factories consume a snapshot DTO (TrackerSnapshot), guaranteeing background threads never disrupt layout iteration or throw collection enumeration exceptions.
  • Blind Push Throttling: Restricting layout broadcasts to 500ms, shields the client app from rendering loops. Heavy view generation runs only when a user is actively staring at the screen and responding to the network call.
  • No Memory Leaks: Placing all background intervals, task hooks, and polling events inside permanent services completely cleans your view controllers, eliminating memory leaks from lingering handlers.
  • Deterministic UI Layouts: Because the active tab factory completely wipes and regenerates your components from a unified state on every update, Emby's lack of stable client element IDs can never cause duplicate entries or misaligned rows.
  • Bulletproof Navigation State: Whether a user stays on the page, switches tabs, or logs out and returns hours later, the freshly built view controller fetches directly from the continuous tracker, drawing accurate layouts every time.

    maybe if none of this is making sense, i will need to provide an example (of code) of the client ui going wrong. thanks again. Implementing some of this thinking has resulted in a stable ui but that could be pure coincidence without understanding.

    image.png.7801e128aa1575b64dcd14e317ffc8ce.png
     
Posted
11 hours ago, ginjaninja said:

So at the risk of outstaying my welcome

You are welcome but I won't even read this again. 

 

11 hours ago, ginjaninja said:

i am trying to understand why when i tell a genericitemlist to have a certain icon or status or button status, the end result of what the user sees, does not feel deterministic (despite the server doing what it needs to do perfectly) eg a row which is NEVER told to have a status of Inprogress in any of the code, ends up having an inprogress status in the client UI (a spinning circle in other words); noise from a nearby row (which was in progress). (I am using UIBaseClasses from the SDK demo).

What you are describing sounds like a bug, possibly on our side. Can you share a minimal repro of this issue?

(public or privately, just as you prefer)

Create an account or sign in to comment

You need to be a member in order to leave a comment

Create an account

Sign up for a new account in our community. It's easy!

Register a new account

Sign in

Already have an account? Sign in here.

Sign In Now
×
×
  • Create New...