The registration hierarchy created in the previous step makes features composable without introducing a separate options model. Every Register... method accepts an optional callback. With no callback, it registers the defaults for that scope; with a callback, the application selects the registrations explicitly.
This section adds a Dogs page and shows how an application can load both pages or only one of them.
Add the Dogs page
Add dog.jpg to the module's Assets folder and register it in the project file:
using Avalonia.Controls;
namespace Asv.Avalonia.Module;
public partial class DogsPageView : UserControl
{
public DogsPageView()
{
InitializeComponent();
}
}
using Asv.Common;
using Avalonia.Media;
using Avalonia.Media.Imaging;
using Avalonia.Platform;
using Material.Icons;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Logging.Abstractions;
namespace Asv.Avalonia.Module;
public class DogsPageViewModel : PageViewModel<DogsPageViewModel>
{
public const string PageId = "dogs";
public const MaterialIconKind PageIcon = MaterialIconKind.Dog;
public const AsvColorKind PageIconColor = AsvColorKind.Info7;
public DogsPageViewModel()
: this(
DesignTime.PageContext,
NullLoggerFactory.Instance,
DesignTime.DialogService,
DesignTime.ExtensionService)
{
DesignTime.ThrowIfNotDesignMode();
}
public DogsPageViewModel(
IPageContext context,
ILoggerFactory loggerFactory,
IDialogService dialogService,
IExtensionService ext)
: base(PageId, context, loggerFactory, dialogService, ext)
{
Header = "Dogs";
Icon = PageIcon;
IconColor = PageIconColor;
var stream = AssetLoader
.Open(new Uri("avares://Asv.Avalonia.Module/Assets/dog.jpg"))
.DisposeItWith(Disposable);
var defaultPicture = new Bitmap(stream).DisposeItWith(Disposable);
SelectedImage = defaultPicture;
}
public IImage? SelectedImage
{
get;
private init => SetField(ref field, value);
}
public override IEnumerable<IViewModel> GetChildren()
{
return [];
}
protected override void AfterLoadExtensions()
{
// No page extensions need post-load processing.
}
}
Add the Dogs home page action
The Dogs action follows the same direct-navigation pattern as the Cats action:
using Asv.Common;
using Asv.Modeling;
using R3;
namespace Asv.Avalonia.Module;
public sealed class HomePageDogsPageExtension : IExtensionFor<IHomePage>
{
public const string StaticId = "ext.home.dogs";
public string Id => StaticId;
public void Extend(IHomePage context, CompositeDisposable contextDispose)
{
var action = new ActionViewModel("open-dogs")
{
Header = "Open dogs page",
Description = "Opens the dogs page",
Icon = DogsPageViewModel.PageIcon,
IconColor = DogsPageViewModel.PageIconColor,
Command = new ReactiveCommand(async (_, _) =>
await context.GoTo(new NavPath(new NavId(DogsPageViewModel.PageId)))
).DisposeItWith(contextDispose),
}.DisposeItWith(contextDispose);
context.Tools.Add(action);
}
}
Register the Dogs feature
Create DogsRegistrations.cs in Shell/Pages/Dogs:
namespace Asv.Avalonia.Module;
public static class DogsRegistrations
{
extension(PagesRegistrations.Builder builder)
{
public PagesRegistrations.Builder RegisterDogs()
{
builder.AppBuilder.Pages.Register<DogsPageViewModel, DogsPageView>(
DogsPageViewModel.PageId
);
builder.AppBuilder.Extensions.Register<IHomePage, HomePageDogsPageExtension>();
return builder;
}
}
}
Update PagesRegistrations.Builder.RegisterDefault() so the default module contains both pages:
public Builder RegisterDefault()
{
this.RegisterDogs();
this.RegisterCats();
return this;
}
No changes are required in the top-level or shell builders. Their default chain reaches PagesRegistrations.Builder.RegisterDefault() automatically.
Select module features
The shortest application setup registers every default feature:
If RegisterModuleModule() is omitted, none of the module's services, pages, or extensions are registered.
This design keeps registration close to each feature and lets new scopes or features participate in the same hierarchy without a central list of Boolean options.