Skip to content

fix(FlexView): copy federated views via the configuration API - #169

Open
Sario27 wants to merge 15 commits into
Cacsjep:mainfrom
Sario27:flex-federal
Open

fix(FlexView): copy federated views via the configuration API#169
Sario27 wants to merge 15 commits into
Cacsjep:mainfrom
Sario27:flex-federal

Conversation

@Sario27

@Sario27 Sario27 commented Jul 29, 2026

Copy link
Copy Markdown

Summary

Copying a view from a child site (in a Milestone Federated Architecture hierarchy) into a local View Group failed with "This view is on another site and could not be resolved through the current session", and browsing federated sites could take 90+ seconds and freeze the client.

Root cause: Views are not part of MFA's federated client-session model - ClientControl/Configuration.Instance.GetItem can never resolve a view living on a child site, unlike devices/cameras/alarms/access, which are genuinely federated.

Fix: read views directly off each site's Management Server configuration (ManagementServer -> ViewGroupFolder -> ViewGroup -> ViewFolder -> View) instead of the client session - this works uniformly for the master and every child site, since it doesn't depend on federation at the client layer at all. The copy is recreated via ViewGroup.ViewFolder.AddView(...) using the captured LayoutViewItems XML, then camera slots are restored afterward through the client session (InsertBuiltinViewItem, the only documented way to write per-slot content) - which now works because the destination is genuinely local.

A few real bugs turned up from testing this against a live 3 MGMT servers, with 130 view groups, all fixed along the way:

  • ViewGroup is ambiguous between VideoOS.Platform.Client and VideoOS.Platform.ConfigurationItems (CS0104)
  • new ViewGroup(fqid) throws "Invalid kind for this constructor" when the FQID comes from a client Item rather than the config-API's own object graph - the destination is now resolved by walking the tree and matching Id instead
  • The site walk ran synchronously and could freeze Smart Client for the whole duration - now backgrounded, with the master's own view tree pre-fetched once instead of re-walked via live GetChildren() calls on every open
  • Restoring camera slots must run on Smart Client's UI thread - doing it off-thread throws "The calling thread cannot access this object because a different thread owns it" on Save()
  • The client's item cache doesn't see a view created through the raw config API immediately (Configuration.RefreshConfiguration explicitly excludes built-in types), so restoring cameras needs a bounded async retry
  • A Save() failure was incorrectly zeroing out an already-successful camera restore in the reported count

Also added:

  • Session-long caching of the site/view walk, with a manual Refresh button, so repeat browsing doesn't re-walk every site
  • Batch copy: check several views across sites, pick one destination folder, one summary result instead of one dialog per view

Test plan

  • Verified end-to-end against a real 3 MGMT Server hierarchy, 1 parent 2 children, 130 view groups.
  • Single-view copy from a child site into a local View Group, including camera slots
  • Batch copy of multiple views (from different sites/groups) into one destination in a single operation
  • Confirmed camera slots carry over correctly (e.g. 4/4, 12/12 cameras across different test views)
  • Confirmed the client no longer freezes while browsing federated sites

Cacsjep and others added 15 commits July 23, 2026 18:05
…esolving to a live item

Configuration.Instance.GetItem() never resolves a child-site view's rebuilt FQID
back to a ViewAndLayoutItem - Views aren't part of MFA's federated client-session
model (only devices/cameras/alarms/access are), so the existing ResolveFederatedView
path always returned null and surfaced as "Open Failed... could not be resolved
through the current session."

FederationWalker already proved View.LayoutViewItems (plus Shortcut/LayoutCustomId/
LayoutIcon/ViewLayoutType) is readable straight off each site's Management Server
config, federated children included. ViewFolder.AddView takes that same raw XML
directly, so a child-site view can now be copied into a folder on this site purely
through VideoOS.Platform.ConfigurationItems, without ever needing a client-side
handle to the source view.
AddView/AddViewGroup return a ServerTask that can still be Progress<100 when the
call returns; per the SDK docs, callers must poll UpdateState() until it completes.
The previous commit called AddView and assumed success unconditionally, so a
server-side failure (duplicate name, permission denied, invalid layout XML) would
have been silently reported as a successful copy.
VideoOS.Platform.Client also defines a ViewGroup type, which collided with
VideoOS.Platform.ConfigurationItems.ViewGroup once both namespaces were
imported. Drop the blanket using and fully qualify the one config-plane
ViewGroup construction instead, matching how ServerTask/StateEnum are
already referenced in this file.
…nd load federated sites off the UI thread

Two bugs found from a real test run:

1. "Copy Failed: Invalid kind for this constructor" - new ViewGroup(fqid) validates
   that the FQID came from the same configuration-API object graph. A client Item's
   FQID (from the folder browse dialog) never satisfies that, regardless of which
   folder is picked. Every other working config-plane read in this codebase reaches
   a ViewGroup by navigating properties from a genuine site FQID, never by
   reconstructing one from a client Item - so the destination is now found by
   walking the config tree and matching ViewGroup.Id instead.

2. Clicking Open View froze Smart Client for ~85 seconds. FederationWalker
   .CollectAllSiteViews() walks every site's configuration over the network,
   synchronously, directly on the UI thread. It's now offloaded to a background
   thread with a "Loading sites..." placeholder shown immediately.
…actually lives

Confirmed against a real copy: layout/panes come through correctly via
LayoutViewItems + AddView, but camera assignments don't. View.ViewItemChildItems
(one per pane, keyed by ViewItemPosition) is a separate structure from
LayoutViewItems and likely carries that content via ViewItemDefinitionXml - dump
it for the first few views per site so the real XML shape is known before writing
parsing/restore code against it.
Confirmed from a real ViewItemDefinitionXml dump: a camera slot looks like
<viewitem type="...CameraViewItem..."><iteminfo cameraid="{guid}" .../></viewitem>,
separate from LayoutViewItems (geometry only). FederationWalker now parses each
view's ViewItemChildItems and captures camera slot positions/ids.

After AddView creates the (empty-slotted) view, the destination is genuinely
local, so - unlike the source - Configuration.Instance.GetItem resolves it fine.
ServerTask.Path gives the new view's config-API path, used to read its Id and
rebuild a client FQID, then InsertBuiltinViewItem(position, CameraBuiltinId,
{CameraId}) restores each camera slot through the exact same pipeline the
same-site copy already uses. CameraBuiltinId was found via reflection (SDK docs
don't list it) alongside the sibling Hotspot/Carousel/Map/etc constants.

Non-camera slot types (maps, HTML, plugins) are left empty for now - camera
grids are the reported use case.
…g cameras

Confirmed from a real run: AddView succeeds and camera slots parse correctly,
but Configuration.Instance.GetItem returns null immediately afterward for the
brand-new (genuinely local) view. The raw config API write goes through a side
channel the client session's own item cache isn't synchronously notified about.

Checked Configuration.RefreshConfiguration as the obvious fix first - its SDK
doc explicitly rules it out for this case: "Only works for plug-in defined
configurations ... built-in item types ... cannot be refreshed", and View is
built-in. Also checked View.ApplyViewToView via IL as a same-server "clone from
another view" alternative - it sets a ViewToApply property expecting a path on
the same management server, which doesn't fit a cross-server source either way.

With no way to force the client cache, retry GetItem with a short delay for up
to ~7 seconds before giving up.
…ses, defer camera reads, and stop freezing the client on Save

Three real problems found from live testing:

1. PopulateFederatedTree renders the master site from ClientViewRoots (the
   client-session tree, already fast/working) and never reads sv.FedViews for
   it - but CollectAllSiteViews was walking the master's full config-plane view
   tree anyway. Confirmed against a real system: that walk alone (3411 views)
   took ~73 of the ~90 second total "Loading sites..." time, entirely wasted.
   Only child sites (which have no working client-session tree - the reason
   this file exists) actually need that walk now.

2. The camera-restore fix in the previous commit read and parsed
   ViewItemChildItems for every view during the same walk, not just the one
   view actually being copied - roughly doubling an already expensive walk.
   FedView now only captures the (already-loaded, free) ServerId/Path needed to
   re-fetch one view's content later; FederationWalker.ReadCameraItems does that
   single fetch at copy time instead.

3. CopyFederatedViewToLocal ran AddView + the server-task poll + the client-cache
   retry synchronously on the UI thread, freezing Smart Client for however long
   all three took (confirmed up to ~45+ seconds in one run). The network-bound
   work now runs via Task.Run; only the two dialogs touch the UI thread.
Confirmed from a real run: AddView, its server-task poll, and reading the
source's camera items (ReadCameraItems) all succeeded fine on the background
thread introduced in the previous commit - but the final Save() threw "The
calling thread cannot access this object because a different thread owns it."

The ViewAndLayoutItem returned by Configuration.Instance.GetItem is a
client-session object with UI-thread affinity, unlike the raw
ConfigurationItems.* objects (ManagementServer, ViewGroup, View, ServerTask)
used everywhere else in this file, which have no such restriction.

Split the work accordingly: PrepareFederatedCopy (AddView + wait + read camera
items) still runs via Task.Run since it's genuinely network-bound and thread-
agnostic; RestoreCameraSlots (resolve the new view + InsertBuiltinViewItem +
Save) now runs back on the UI thread after the await, bounded to a few seconds
by its own retry cap rather than the whole operation's much longer duration.
…ailure, and stop blocking the UI for up to 46s while waiting

Two things found by comparing two real runs:

1. On the run that hit the thread-affinity bug (Save() called from the wrong
   thread), the resulting view actually had working cameras despite the plugin
   reporting "0/4 restored" - InsertBuiltinViewItem's effects evidently persist
   independent of the subsequent Save() call. RestoreCameraSlots was resetting
   the whole restored count to 0 whenever that final Save() threw, which
   undercounted copies that had actually worked. A Save() failure is now logged
   but no longer discards an already-successful restore count.

2. On the next run (now correctly running on the UI thread per the previous
   commit), the client-cache retry legitimately took 45.9 seconds before giving
   up - individual Configuration.Instance.GetItem calls are themselves slow
   (~2-3s each, real network round-trips), not just the delay between them, and
   my 7.5-second cap was nowhere near enough. That whole wait ran via
   Thread.Sleep, fully blocking Smart Client's UI thread. The retry loop is now
   async (Task.Delay instead of Thread.Sleep, deadline raised to 90s) so the UI
   stays responsive between attempts, even though each individual attempt still
   has to run on this thread and takes however long the network call takes.
…dd manual Refresh

Confirmed working end-to-end, but every Open View click re-walked every child
site's full view tree from scratch, even though views rarely change site-to-site
within a single Smart Client session. FederationWalker.CollectAllSiteViews now
caches its result for the session; a Refresh button (only shown in federated
Open View mode) calls FederationWalker.ForceRefresh() and re-walks on demand for
the rare case something changed on another site.

Deliberately no automatic/time-based expiry - manual refresh only, per explicit
direction.
…tually instant

The session cache from the previous commit skipped re-walking site configs, but
PopulateFederatedTree was still calling CreateTreeNode(root) for the master's
~113+ view groups on every single Open View click - that recursively calls
Item.GetChildren() live, over the network, for every node. That recursive fetch
was the ~10-second cost still showing up after the cache "hit".

FederationWalker.BuildClientNode now resolves that whole tree once, up front,
while the site is walked (same place ClientViewRoots was already being
populated). ViewBrowserWindow builds TreeViewItems from that cached structure
(CreateTreeNodeFromClientNode) instead of calling GetChildren() itself, so
rebuilding the on-screen tree from a cache hit is pure in-memory recursion.
Each openable child-site view in the browse tree now has a checkbox. Checking
one or more switches btnSelect into "Copy N Selected" mode; confirming prompts
for a single destination folder (not one per view) and copies each selected
view into it, keeping each view's own name auto-deduped via UniqueName rather
than prompting per view. One summary dialog covers the whole batch instead of
one dialog per view, and a failure on one view doesn't stop the rest.

DestinationInfo/ResolveDestination factor out what used to be per-copy work
(resolving the ViewGroup, walking its existing view names) so a batch of N
views only pays that cost once instead of N times. The existing single-view
Select flow (SelectedFedView) is unchanged and still works when nothing is
checked.
@Sario27
Sario27 requested a review from Cacsjep as a code owner July 29, 2026 20:39
@Cacsjep

Cacsjep commented Jul 29, 2026

Copy link
Copy Markdown
Owner

@Sario27 oh wow awesome bro :) let me review this tomorrow. Thank you for back reporting and creating a PR 🤠

@Cacsjep

Cacsjep commented Jul 30, 2026

Copy link
Copy Markdown
Owner

@Sario27 how long does it now takes to load all sites ?

@Sario27

Sario27 commented Jul 30, 2026

Copy link
Copy Markdown
Author

@Sario27 how long does it now takes to load all sites ?

For 130 view groups across 3 MGMT servers it took about 90 seconds for the first time, after that if you open it again it only take about 5 seconds

@Cacsjep

Cacsjep commented Jul 31, 2026

Copy link
Copy Markdown
Owner

@Sario27 did you try to parallelize the view group walking ?

@Sario27

Sario27 commented Jul 31, 2026

Copy link
Copy Markdown
Author

@Sario27 did you try to parallelize the view group walking ?

I didn't let me look into that

@Cacsjep

Cacsjep commented Jul 31, 2026

Copy link
Copy Markdown
Owner

@Sario27 should be possible to drop the 90 seconds or does the smart client also take so long to load ?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants