-
Notifications
You must be signed in to change notification settings - Fork 2
content page base
All pages created with Shield MVVM MUST inherit from ContentPageBase<>. Each page is tied directly with its View Model in order to support View Model to View Model navigation. This means that the XAML markup must also define the model it is linked to due to automatic code generation of the backing class that contains all of the controls. Thus, all pages created will have a general structure of the following XAML:
<base:ContentPageBase
xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
xmlns:base="clr-namespace:CoreBTS.Maui.ShieldMVVM.Pages;assembly=CoreBTS.Maui.ShieldMVVM"
x:Class="MauiSample.Features.Main.MainPage"
xmlns:vm="clr-namespace:MauiSample.Features.Main"
x:TypeArguments="vm:MainPageViewModel"
x:DataType="vm:MainPageViewModel"
>
</base:ContentPageBase>
xmlns, xmlns:x, and xmls:base would always be the same, regardless of the page created. x:Class is the page that was just created. xmlmns:vm defines the location where the ViewModel the page inherits from is located. x:TypeArguments are used to fill in the generic part of ContentPageBase<> with the View Model this page is tied to. Finally, while not necessary due to the bindings now defined in the code-behind, x:DataType defines what View Model the page's Intellisense should use when filling in data for {Binding} in the XAML file.
Feel free to change base/vm prefixes to whatever standard is normally used.
One of the major differences between Shield MVVM and all other XAML based technology is that developers need to put any binding/converter logic in the code-behind of the XAML file. For some developers, this may feel awkward as they are used to using the {Binding} syntax throughout the XAML itself. While a lot of support for XAML bindings has been added over the years, there are still a few key cons. First of all, IDEs cannot "Find all references" from inside the XAML itself. On the flip side, "Find all references" from the code will not show the property being used on the XAML side. Thus, renaming a property on a View Model will not automatically update the Binding in the XAML. This leads to lots of maintenance issues down the road. In fact, if you run MAUI for Windows in debug mode, a little ribbon at the top shows up where one of the items tells developers how many binding failures it detected!
With Shield MVVM, this is not possible. Developers can go to definition, rename, find all references, etc. and they will all work as it's C#, type-safe code. The code wouldn't build if there were any mistakes. They also get enhanced Intellisense where it will force a developer to use a converter when it detects a type mismatch or the code won't compile. The converters the user can choose from will also show up in Intellisense where only the converters that match both types will appear. Developers can still use the {Binding} syntax with Shield MVVM as it doesn't interfere with that, but the type-safety and Intellisense gains from Shield MVVM only apply in the code-behind. Also, the generic converters used by Shield MVVM will not be accessible by XAML. XAML requires a full, direct class reference to hook everything up.
The following is a small example of what a code-behind file would look like, utilizing Shield MVVM's style.
public partial class MainPage : ContentPageBase<MainPageViewModel>
{
public MainPage(MainPageViewModel viewModel) : base(viewModel)
{
}
protected override void SetupBindings()
{
Binder.WithControl(CounterBtn)
.For(c => c.BindText(), vm => vm.ButtonText)
.Once(c => c.BindClick(), vm => vm.ClickCommand);
Binder.WithControl(SomeLabel)
.Once(c => c.BindText(), vm => "Can be hard-coded if .Once");
Binder.WithControl(NumberCounter)
.For(c => c.BindText(), vm => vm.Counter, c => c.ConvertToString());
Binder.WithControl(SecondaryLabel)
.For(c => c.BindText(), vm => vm.Secondary.MyLabel);
Binder.WithControl(AboutPageButton)
.Once(c => c.BindClick(), vm => vm.AboutPageCommand);
Binder.WithControl(AboutAlternatePageButton)
.Once(c => c.BindClick(), vm => vm.AboutAlternatePageCommand);
Binder.WithControl(Dialog1)
.Once(c => c.BindClick(), vm => vm.Dialog1Command);
Binder.WithControl(Dialog2)
.Once(c => c.BindClick(), vm => vm.Dialog2Command);
}
}
The base class must be ContentPageBase<> with the VM you defined in the XAML. The View Model will be IoC injected into the Page and that View Model will be passed to the base class where all the setup logic will run for the developer. After that, the only method a developer needs to properly fill in is the SetupBindings method.
This method will run as part of the constructor logic, unless you override RunSetupBindings property to return false. In that case, the developer must manually call SetupBindings themselves at some point in the future.
Inside of SetupBindings, the Binder helper property should be called. First, Binder.WithControl is called and the control being bound is passed in. Then, .Once is called if the property is set once and never changes or .For is called to do a true binding setup.
See BindingHelper for more details.
In order to avoid tons of boiler plate code in the constructor, InitializeComponent is automatically called via reflection. Since this is a one-time call for the entire page's lifecycle, this doesn't really add any overhead.
MAUI allows a page to be wrapped with another page called a Control Template. If the page needs to be wrapped, override the SetControlTemplate and set the ControlTemplate property with the proper value. The constructor calls it in a specific order to make sure everything is hooked up correctly.
In order to support the advanced scenario of allowing View Model inheritance, a small sacrifice had to be made when it came to using pages and IoC. If a developer asks the IoC container for a page, it will only new up the page with the View Model that is defined in its constructor... Even though the NavigationService knows the developer wanted a different View Model that inherited from it. To get around this problem, the NavigationService will ask the IoC container for the ViewModel and then use reflection to get the constructor of the page, building the parameters by asking the IoC container directly per parameter and give the constructor the specific View Model instead of the one the page would have produced. This works great and there aren't any issues with it. However, there is one limitation with this. That means if a page is registered by the developer in the IoC container that "news" it up in a specific way, that will be ignored. Only parameters that can be obtained from the IoC container will work. That being said, there shouldn't really be any reason why any needed parameter into a page constructor couldn't be registered properly. For everything else, the data should be sent via a TParameter via the NavigationService call.