4. SusRouteView — визуальный контейнер
API
SusRouteView — подкласс базового примитива SusScreenOutlet<SusScreen>, navigation-агностичного контейнера слоя экранов. Он лишь добавляет route-специфичную логику; монтирование + инфраструктура KeepAlive (LRU) живут в core.
public class SusRouteView : SusScreenOutlet<SusScreen>
{
public SusRouter Router { get; set; }
// Inherited from SusScreenOutlet<SusScreen>:
public int MaxKeepAlive { get; set; } = 10;
public SusScreen CurrentScreen { get; protected set; }
public void OnRouteChanged(SusRoute fromRoute, SusRoute toRoute);
public void RenderNestedChain(List<SusScreen> chainScreens);
}Поток
SusRouter.Navigate(from, to)
|
SusRouteView.OnRouteChanged(from, to):
1. if !KeepAlive → from.Screen.parent.Remove(from.Screen) ← detach + drop
if KeepAlive → Remove(from.Screen) + CacheKeepAliveScreen(key, screen) ← detach + cache
2. Add(to.Screen) ← add new/cached screen
3. to.Screen.style.flexGrow = 1; CurrentScreen = to.ScreenПереходы (PlayIn/PlayOut) управляются SusRouter вокруг этой замены.
KeepAlive — off-DOM LRU-кэш
router.Register("/battle/:id", typeof(BattleScreen), new SusRouteConfig
{
KeepAlive = true,
Transition = SusRouteTransition.Fade(0.2f)
});Как это работает
KeepAlive — это off-DOM кэш, а не оборачивающий элемент: экран отсоединяется от outlet'а (parent.Remove(screen)), а не скрывается через display:none и не оборачивается в базовый компонент SusKeepAlive.
- При уходе с KeepAlive-экрана:
OnRouteChangedудаляет экран из outlet'а и вызываетCacheKeepAliveScreen(key, screen). Отсоединённое поддерево VisualElement и его состояниеProp<T>сохраняются в памяти.fromRoute.Screenочищается,IsActive = false. - При возврате: router запрашивает
TryGetKeepAliveScreen(key, out screen); при попадании закэшированный инстанс повторно добавляется в outlet — без нового инстанса, безLeft(). Ключ —SusRouter.KeepAliveKey(route)(fallbackroute.FullPath, включая query-параметры). - В router: если у целевого KeepAlive-маршрута уже есть живой закэшированный экран, он переиспользуется вместо создания нового.
LRU-вытеснение
Кэш и порядок хранятся в SusScreenOutlet<TScreen>:
private readonly Dictionary<string, TScreen> _keepAliveCache;
private readonly List<string> _keepAliveOrder; // LRU: front = oldest
public int MaxKeepAlive { get; set; } = 10;Когда добавление нового ключа превысило бы MaxKeepAlive, первыми вытесняются самые старые записи:
_keepAliveOrder[0](самый старый) удаляется из списка порядка;- выполняется
OnScreenEvicted(oldScreen)—SusRouteViewпереопределяет его, вызываяscreen.Left()(teardown); - запись удаляется из
_keepAliveCache.
ClearKeepAliveCache() вытесняет всё (запуская teardown-хук для каждого).
Использование в App.sharq
<template>
<ui:VisualElement $MainElement class="app">
<sus:SusRouteView ref="RouteView" />
</ui:VisualElement>
</template>
<script>
public SusRouter Router = new();
public SusRouteView RouteView;
public override void Mounted()
{
Router.SetRouteView(RouteView);
RouteView.Router = Router;
Router.Register("/main-menu", typeof(MainMenuScreen));
Router.Register("/battle", typeof(BattleHudScreen),
new SusRouteConfig { KeepAlive = true });
Router.Register("/loading", typeof(LoadingScreen));
Router.Replace("/loading");
}
</script>Вложенные маршруты — вложенный рендер
Каждый родительский экран может зарегистрировать вложенный SusRouteView:
public class SettingsScreen : SusScreen
{
public SusRouteView ChildRouterView;
protected override void Build()
{
ChildRouterView = new SusRouteView();
RegisterChildView(ChildRouterView);
Add(ChildRouterView);
}
}При навигации на /settings/profile:
ResolveChain()строит цепочку: SettingsRecord → ProfileRecord- Корневой
SusRouteViewрендеритSettingsScreen SettingsScreen.ChildRouterViewрендеритProfileTabScreen
Ограничение
Когда дочерний маршрут меняется (/settings/profile → /settings/account), родитель пересоздаётся. Diffing на уровне цепочки запланирован на будущее.