3. SusScreen — экраны, жизненный цикл и подключение
Экран (SusScreen) — полноэкранное представление, которым управляет SusRouter. Он наследует SusComponent, поэтому обладает полным core-набором реактивности (Prop<T>, Watch, Build(), Mounted()), плюс жизненный цикл роутера и доступ к параметрам маршрута.
1. Минимальный экран
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 virtualOn*(паттерн template-method).
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. Читайте их через хелперы:
// 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| Член | Тип | Назначение |
|---|---|---|
Router | SusRouter | владелец экрана (навигация, модалки) |
Props | Dictionary<string,object> | params + query + переданные props (никогда не null) |
IsActive | bool | активен ли экран сейчас |
GetProp<T>(key, def) | T | типизированное чтение с конвертацией |
GetParam(key, def) / GetQuery(key, def) | string | алиасы в Props (params/query уже объединены) |
4. Подключение экрана — три способа
4.1 Императивно — router.Register(path, type, config?)
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
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)
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
| Опция | Тип | Что делает |
|---|---|---|
Name | string | имя для PushNamed/ReplaceNamed |
KeepAlive | bool | не пересоздавать экран при выходе (кэш инстанса) |
Alias | List<string> | дополнительные пути, ведущие на этот маршрут |
Children | List<SusRouteRecord> | вложенные маршруты (один уровень) |
Redirect | string | при входе — перейти сюда вместо этого маршрута |
DefaultProps | Dictionary<string,object> | props по умолчанию для экрана |
PropsFn | Func<SusRoute, Dictionary<string,object>> | генератор props из маршрута (аналог Vue props: route => ({...})) |
LazyFactory | Func<SusScreen> | отложенное создание экрана (вместо Activator.CreateInstance) |
Guard | ISusRouteGuard | per-route guard CanEnter/CanLeave |
BeforeEnter | SusRouterGuard | функциональный guard входа (после Guard.CanEnter) |
Transition | SusRouteTransition | анимация перехода (Fade(), SlideLeft(), …) |
Meta | Dictionary<string,object> | произвольные метаданные (requiresAuth, title) |
CaseSensitive | bool | регистрозависимое сопоставление пути (по умолчанию выкл.) |
Strict | bool | завершающий слэш имеет значение (/a ≠ /a/) |
Примеры для каждой опции
// 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'ом / редирект).
if (Router.CanGoBack) Router.Back();
Router.Push("/checkout", new() { ["cartId"] = cart.Id });
Router.Modal(typeof(InfoDialog), new() { ["text"] = "Done!" });7. Вложенные экраны
Родительский экран регистрирует дочерний SusRouteView, в который роутер монтирует дочерние экраны:
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)
[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()не вызывается.
Связанные документы
- 02-router-api.md — полный API
SusRouter - 04-routeview.md —
SusRouteViewи KeepAlive - 05-modals.md — модалки
- 06-guards-transitions.md — конвейер guard'ов и анимации