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

3. SusScreen — экраны, жизненный цикл и подключение

Экран (SusScreen) — полноэкранное представление, которым управляет SusRouter. Он наследует SusComponent, поэтому обладает полным core-набором реактивности (Prop<T>, Watch, Build(), Mounted()), плюс жизненный цикл роутера и доступ к параметрам маршрута.


1. Минимальный экран

csharp
using UnityEngine.UIElements;
using Sharq.Router;

public class HomeScreen : SusScreen
{
    protected override void Build()
    {
        style.flexGrow = 1f;
        Add(new Label("Home"));

        var btn = new Button { text = "Go to About" };
        btn.RegisterCallback<ClickEvent>(_ => Router.Push("/about"));
        Add(btn);
    }
}

Build() строит дерево (из SusComponent). Router и Props ещё не установлены в Build() — читайте их из OnBeforeEnter() / OnEntered().


2. Жизненный цикл — переопределяем методы On*

⚠️ Важно: публичные BeforeEnter/Entered/BeforeLeave/Left/BeforeRouteUpdate вызываются самим роутером — не переопределяйте их. Переопределяйте хуки protected virtual On* (паттерн template-method).

csharp
public class BattleScreen : SusScreen
{
    // 1. Enter validation + reading Props. false = enter cancelled.
    protected override bool OnBeforeEnter(SusRoute fromRoute)
    {
        var matchId = GetParam("matchId");
        return !string.IsNullOrEmpty(matchId);
    }

    // 2. Screen is already in the DOM — start animations, load data.
    protected override void OnEntered() => StartBattle();

    // 3. Props changed on the SAME screen instance (e.g. /users/1 → /users/2).
    protected override bool OnBeforeRouteUpdate(SusRoute toRoute) => true;

    // 4. Leave guard. false = navigation blocked (e.g. dirty form).
    protected override bool OnBeforeLeave(SusRoute toRoute)
    {
        if (_isDirty)
        {
            Router.Modal(typeof(ConfirmLeaveDialog), new() { ["message"] = "Leave without saving?" });
            return false;
        }
        return true;
    }

    // 5. Screen is being removed — unsubscribe, clean up resources.
    protected override void OnLeft() => CleanupBattle();
}

Полная последовательность

Push("/battle/42"):
  1. Activator.CreateInstance / LazyFactory()
       Created()              ← SusComponent; Router/Props NOT set yet
       Build()                ← tree is built
  2. screen.Router = router
  3. screen.Props = params + query + DefaultProps + PropsFn(...)
  4. OnBeforeEnter(from)      ← false = cancel entire navigation
  5. history updates, SusRouteView swaps screen + transition.PlayIn()
  6. OnEntered()             ← screen in DOM
  7. (next frame) Mounted()   ← deferred SusComponent hook

Leave:
  8. OnBeforeLeave(to)       ← guard, false = cancel
  9. OnLeft()
 10. SusRouteView removes screen (or hides on KeepAlive)
 11. Unmounted()            ← SusComponent; NOT called on KeepAlive

При KeepAlive экран не пересоздаётся: OnLeft() выполняется, но инстанс скрывается (не уничтожается); при возврате — снова OnBeforeEnter() / OnEntered() без нового Build().


3. Доступ к данным маршрута

SusRouter объединяет параметры пути (:id) и query (?tab=x) в один словарь Props. Читайте их через хелперы:

csharp
// route "/users/:id",  URL "/users/42?tab=profile"
string id  = GetParam("id");          // "42"      (path param)
string tab = GetQuery("tab");         // "profile" (query)
int    page = GetProp("page", 1);     // typed, with default
object raw = GetProp("payload");      // untyped
ЧленТипНазначение
RouterSusRouterвладелец экрана (навигация, модалки)
PropsDictionary<string,object>params + query + переданные props (никогда не null)
IsActiveboolактивен ли экран сейчас
GetProp<T>(key, def)Tтипизированное чтение с конвертацией
GetParam(key, def) / GetQuery(key, def)stringалиасы в Props (params/query уже объединены)

4. Подключение экрана — три способа

4.1 Императивно — router.Register(path, type, config?)

csharp
var router = new SusRouter();
router.Register("/", typeof(HomeScreen));
router.Register("/about", typeof(AboutScreen));
router.Register("/users/:id", typeof(UserScreen),
    new SusRouteConfig { Name = "user", KeepAlive = true });
router.Mount(root, "/");     // or Mount(uiDocument, "/")

Register возвращает SusRouteRecord — его можно вложить в родительский Children (см. §5, вложенные).

4.2 Декларативно — SusRouteBuilder через SusApp.UseRouter

csharp
SusApp.Create(uiDocument)
      .UseTheme(SusTheme.Dark)
      .UseRouter(new SusRouter(), routes => routes
          .Route("/", typeof(HomeScreen)).Name("home")
          .Route<LoginScreen>("/login").Alias("/signin")
          .Route("/users/:id", typeof(UserScreen))
              .Name("user").KeepAlive().Meta("requiresAuth", true)
              .Children(c => c
                  .Route("profile", typeof(ProfileScreen))
                  .Route("posts", typeof(PostsScreen))),
          initialPath: "/")
      .Run();

4.3 Императивный UseRouter (Register внутри SusApp)

csharp
SusApp.Create(uiDocument)
      .UseRouter(new SusRouter(), r =>
      {
          r.Register("/", typeof(HomeScreen));
          r.Register("/settings", typeof(SettingsScreen));
      }, initialPath: "/")
      .Run();

UseRouter встраивает регистрацию + Mount в нужную точку финализации SusApp (после token cascade / custom styles, до применения темы).


5. Все опции SusRouteConfig

ОпцияТипЧто делает
Namestringимя для PushNamed/ReplaceNamed
KeepAliveboolне пересоздавать экран при выходе (кэш инстанса)
AliasList<string>дополнительные пути, ведущие на этот маршрут
ChildrenList<SusRouteRecord>вложенные маршруты (один уровень)
Redirectstringпри входе — перейти сюда вместо этого маршрута
DefaultPropsDictionary<string,object>props по умолчанию для экрана
PropsFnFunc<SusRoute, Dictionary<string,object>>генератор props из маршрута (аналог Vue props: route => ({...}))
LazyFactoryFunc<SusScreen>отложенное создание экрана (вместо Activator.CreateInstance)
GuardISusRouteGuardper-route guard CanEnter/CanLeave
BeforeEnterSusRouterGuardфункциональный guard входа (после Guard.CanEnter)
TransitionSusRouteTransitionанимация перехода (Fade(), SlideLeft(), …)
MetaDictionary<string,object>произвольные метаданные (requiresAuth, title)
CaseSensitiveboolрегистрозависимое сопоставление пути (по умолчанию выкл.)
Strictboolзавершающий слэш имеет значение (/a/a/)

Примеры для каждой опции

csharp
// KeepAlive + Transition
router.Register("/feed", typeof(FeedScreen),
    new SusRouteConfig { KeepAlive = true, Transition = SusRouteTransition.Fade() });

// Named + params → PushNamed
router.Register("/users/:id", typeof(UserScreen), new SusRouteConfig { Name = "user" });
router.PushNamed("user", new() { ["id"] = "42" });

// Alias + Redirect
router.Register("/home", typeof(HomeScreen), new SusRouteConfig { Alias = new() { "/start" } });
router.Register("/old", typeof(HomeScreen), new SusRouteConfig { Redirect = "/home" });

// DefaultProps + PropsFn
router.Register("/report", typeof(ReportScreen), new SusRouteConfig
{
    DefaultProps = new() { ["format"] = "pdf" },
    PropsFn = route => new() { ["id"] = route.Params.GetValueOrDefault("id") },
});

// Lazy + Meta + Guard
router.Register("/admin", typeof(AdminScreen), new SusRouteConfig
{
    LazyFactory = () => new AdminScreen(),
    Meta = new() { ["requiresAuth"] = true },
    BeforeEnter = (from, to) => Session.IsAdmin,
});

// Nested (Children) via Register records
var users = router.Register("/users", typeof(UsersScreen), new SusRouteConfig
{
    Children = new()
    {
        router.Register("/users/:id", typeof(UserDetailScreen)),
        router.Register("/users/search", typeof(UserSearchScreen)),
    }
});

6. Навигация (методы SusRouter)

МетодНазначение
Push(path, props?)добавить в историю и перейти
Replace(path, props?)заменить текущую запись
PushNamed(name, pathParams?, props?)перейти по имени маршрута
ReplaceNamed(name, pathParams?, props?)заменить по имени
Back() / Forward()история (на основе курсора)
Go(n)смещение на n шагов
NavigateWithTransition(path, …)переход с явной анимацией
CanGoBack / CanGoForwardдоступность истории (для кнопок)
Modal(type, props?) / CloseModal()модалки через ModalService

Все Push*/Replace*/Back/Forward/Go возвращают NavigationResult (успех / отмена guard'ом / редирект).

csharp
if (Router.CanGoBack) Router.Back();
Router.Push("/checkout", new() { ["cartId"] = cart.Id });
Router.Modal(typeof(InfoDialog), new() { ["text"] = "Done!" });

7. Вложенные экраны

Родительский экран регистрирует дочерний SusRouteView, в который роутер монтирует дочерние экраны:

csharp
public class UsersScreen : SusScreen
{
    protected override void Build()
    {
        Add(new Label("Users"));

        var childView = new SusRouteView();
        RegisterChildView(childView);   // router mounts /users/:id etc. here
        Add(childView);
    }
}

ChildView (первый) и ChildViews (все) доступны из кода. Разрешение цепочки выбирает MatchedChain записей от корня до листа.


8. Точка входа (MonoBehaviour)

csharp
[RequireComponent(typeof(UIDocument))]
public class AppEntry : MonoBehaviour
{
    [SerializeField] private UIDocument _uiDocument;

    private void OnEnable()
    {
        // Create(UIDocument) applies SusDefault.tss to the panel; prefer it over
        // Create(rootVisualElement), which skips TSS.
        SusApp.Create(_uiDocument)
              .UseTheme(SusTheme.Dark)
              .UseRouter(new SusRouter(), routes => routes
                  .Route("/main-menu", typeof(MainMenuScreen)).Name("menu")
                  .Route("/battle/:matchId", typeof(BattleScreen))
                      .KeepAlive().Transition(SusRouteTransition.Fade()),
                  initialPath: "/main-menu")
              .Run();
    }
}

Интеграция с SusComponent

  • OnEntered() вызывается перед Mounted(). Логику, которой нужны дочерние элементы, отсутствующие на момент Build(), кладите в Mounted().
  • Router/Props доступны с OnBeforeEnter(), но не в Created()/Build().
  • Unmounted() — при отсоединении от панели (после OnLeft()); при KeepAlive экран скрывается, и Unmounted() не вызывается.

Связанные документы