К содержимому

4. SusRouteView — визуальный контейнер

API

SusRouteView — подкласс базового примитива SusScreenOutlet<SusScreen>, navigation-агностичного контейнера слоя экранов. Он лишь добавляет route-специфичную логику; монтирование + инфраструктура KeepAlive (LRU) живут в core.

csharp
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-кэш

csharp
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.

  1. При уходе с KeepAlive-экрана: OnRouteChanged удаляет экран из outlet'а и вызывает CacheKeepAliveScreen(key, screen). Отсоединённое поддерево VisualElement и его состояние Prop<T> сохраняются в памяти. fromRoute.Screen очищается, IsActive = false.
  2. При возврате: router запрашивает TryGetKeepAliveScreen(key, out screen); при попадании закэшированный инстанс повторно добавляется в outlet — без нового инстанса, без Left(). Ключ — SusRouter.KeepAliveKey(route) (fallback route.FullPath, включая query-параметры).
  3. В router: если у целевого KeepAlive-маршрута уже есть живой закэшированный экран, он переиспользуется вместо создания нового.

LRU-вытеснение

Кэш и порядок хранятся в SusScreenOutlet<TScreen>:

csharp
private readonly Dictionary<string, TScreen> _keepAliveCache;
private readonly List<string> _keepAliveOrder; // LRU: front = oldest
public int MaxKeepAlive { get; set; } = 10;

Когда добавление нового ключа превысило бы MaxKeepAlive, первыми вытесняются самые старые записи:

  1. _keepAliveOrder[0] (самый старый) удаляется из списка порядка;
  2. выполняется OnScreenEvicted(oldScreen)SusRouteView переопределяет его, вызывая screen.Left() (teardown);
  3. запись удаляется из _keepAliveCache.

ClearKeepAliveCache() вытесняет всё (запуская teardown-хук для каждого).

Использование в App.sharq

xml
<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:

csharp
public class SettingsScreen : SusScreen
{
    public SusRouteView ChildRouterView;

    protected override void Build()
    {
        ChildRouterView = new SusRouteView();
        RegisterChildView(ChildRouterView);
        Add(ChildRouterView);
    }
}

При навигации на /settings/profile:

  1. ResolveChain() строит цепочку: SettingsRecord → ProfileRecord
  2. Корневой SusRouteView рендерит SettingsScreen
  3. SettingsScreen.ChildRouterView рендерит ProfileTabScreen

Ограничение

Когда дочерний маршрут меняется (/settings/profile/settings/account), родитель пересоздаётся. Diffing на уровне цепочки запланирован на будущее.