diff --git a/CONTRIBUTING.MD b/CONTRIBUTING.MD index 7064b18..065c9af 100644 --- a/CONTRIBUTING.MD +++ b/CONTRIBUTING.MD @@ -33,4 +33,5 @@ NOTE: The above software is the recommended basics. You may be able to use other ## Submitting a PR - Try to rebase (`git rebase -i`) your PR onto `main` to tidy your git history https://www.atlassian.com/git/tutorials/rewriting-history/git-rebase - Update the unit tests (fix any existing tests and ensure new code has unit test coverage) +- Update the sample app with the change/feature in action - Ensure you have the [XAML Styler](https://marketplace.visualstudio.com/items?itemName=TeamXavalon.XAMLStyler2022) extension installed and have run it on any XAML files you have changed to format them \ No newline at end of file diff --git a/README.md b/README.md index c60c35e..d338117 100644 --- a/README.md +++ b/README.md @@ -147,7 +147,8 @@ ServiceResolver.Resolve(IExampleService); ``` ## Navigation service -`INavigationService` is automatically registered by `.UseBurkusMvvm(...)`. You can use it to: push pages, pop pages, pop to the root page, replace the top page of the app, reset the navigation stack, switch tabs, and more. +`INavigationService` is automatically registered by `.UseBurkusMvvm(...)`. You can use it to: push pages, pop pages, pop to the root page, replace the top page of the app, reset the navigation stack, switch tabs, and more. +See the [INavigationService interface in the repository](https://github.com/BurkusCat/Burkus.Mvvm.Maui/blob/main/src/Abstractions/INavigationService.cs) for all possible navigation method options. This is a simple navigation example where we push a "`TestPage`" onto the navigation stack: ``` csharp @@ -164,7 +165,7 @@ var navigationParameters = new NavigationParameters { "username", Username }, }; -// 2. append an additional custom parameter +// 2. append an additional, custom parameter navigationParameters.Add("selection", Selection); // 3. reserved parameter with a special meaning in the Burkus MVVM library, it has a helper method to make setting it easier @@ -173,7 +174,55 @@ navigationParameters.UseModalNavigation = true; await navigationService.Push(navigationParameters); ``` -See the [INavigationService interface in the repository](https://github.com/BurkusCat/Burkus.Mvvm.Maui/blob/main/src/Abstractions/INavigationService.cs) for all possible navigation method options. +The `INavigationService` supports URI/URL-based navigation. Use the `.Navigate(string uri)` or `.Navigate(string uri, NavigationParameters navigationParameters)` methods to do more complex navigation. + +**⚠️ WARNING**: URI-based navigation behavior is unstable and is likely to change in future releases. Passing parameters, events triggered etc. are all inconsistent at present. + +Here are some examples of URI navigation: +``` csharp +// use absolute navigation (starts with a "/") to go to the LoginPage +navigationService.Navigate("/LoginPage"); + +// push multiple pages using relative navigation onto the stack +navigationService.Navigate("AlphaPage/BetaPage/CharliePage"); + +// push a page relatively with query parameters +navigationService.Navigate("HomePage?username=Ronan&loggedIn=True"); + +// push a page with query parameters *and* navigation parameters +// - the query parameters only apply to one segment +// - the navigation parameters apply to the entire navigation +// - query parameters override navigation parameters +var parameters = new NavigationParameters { "example", 456 }; +navigationService.Navigate("ProductPage?productid=123", parameters); + +// go back one page modally +var parameters = new NavigationParameters(); +parameters.UseModalNavigation = true; +navigationService.Navigate("..", parameters); + +// go back three pages and push one new page +navigationService.Navigate("../../../AlphaPage"); + +// it is good practice to use nameof(x) to provide a compile-time reference to the pages in your navigation +navigationService.Navigate($"{nameof(YankeePage)}/{nameof(ZuluPage)}"); +``` +### Navigation URI builder +Navigation to multiple pages simultaneously and passing parameters to them can start to get complicated quickly. The `NavigationUriBuilder` is a simple, typed way to build a complex navigation string. + +Below is an example where we go back a page (and pass a parameter that instructs the navigation to be performed modally), then push a `VictorPage`, and then push a `YankeePage` modally onto the stack: +``` csharp +var parameters = new NavigationParameters(); +parameters.UseModalNavigation = true; + +var navigationUri = new NavigationUriBuilder() + .AddGoBackSegment(parameters) + .AddSegment() + .AddSegment(parameters) + .Build() // produces the string: "..?UseModalNavigation=True/VictorPage/YankeePage/" + +navigationService.Navigate(navigationUri); +``` ## Choosing the start page of your app It is possible to have a service that decides which page is most appropriate to navigate to. This service could decide to: @@ -236,11 +285,17 @@ If your viewmodel inherits from this interface, the below events will trigger fo Several parameter keys have been pre-defined and are using by the `Burkus.Mvvm.Maui` library to adjust how navigation is performed. - `ReservedNavigationParameters.UseAnimatedNavigation` - - Should an animation be used during the navigation? - - Defaults to: `true` + - If true, uses an animation during navigation. + - Type: `bool` + - Default: `true` - `ReservedNavigationParameters.UseModalNavigation` - - Should the navigation be performed modally? - - Defaults to: `false` + - If true, performs the navigation modally. + - Type: `bool` + - Default: `false` +- `ReservedNavigationParameters.SelectTab` + - If navigating to a `TabbedPage`, selects the tab with the name of the type passed. **⚠️ WARNING**: Not yet implemented. + - Type: `string` + - Default: `null` The `NavigationParameters` object exposes some handy properties `.UseAnimatedNavigation` and `.UseModalNavigation` so you can easily set or check the value of these properties. @@ -274,7 +329,6 @@ The below are some things of note that may help prevent issues from arising: - When you inherit from `BurkusMvvmApplication`, the `MainPage` of the app will be automatically set to a `NavigationPage`. This means the first page you push can be a `ContentPage` rather than needing to push a `NavigationPage`. This may change in the future. # Roadmap 🛣️ -- [URL-based navigation](https://github.com/BurkusCat/Burkus.Mvvm.Maui/issues/1) - [View and viewmodel auto-registration](https://github.com/BurkusCat/Burkus.Mvvm.Maui/issues/4) - [Popup pages](https://github.com/BurkusCat/Burkus.Mvvm.Maui/issues/2) - [Nested viewmodels](https://github.com/BurkusCat/Burkus.Mvvm.Maui/issues/5) diff --git a/SECURITY.MD b/SECURITY.MD index d923db5..8e6bb80 100644 --- a/SECURITY.MD +++ b/SECURITY.MD @@ -10,5 +10,6 @@ The latest version of the package only will be supported with security fixes. If | < 0.1 | :x: | ## Reporting a Vulnerability - -Get in touch with @BurkusCat on Twitter/X. After making contact communicate via Direct Messages to disclose the vulnerability. \ No newline at end of file +Contact options: +- Go to https://www.nuget.org/packages/Burkus.Mvvm.Maui and press the "Contact owners" +- Get in touch with @BurkusCat on Twitter/X. After making contact, communicate via Direct Messages to disclose the vulnerability. \ No newline at end of file diff --git a/samples/DemoApp/App.xaml b/samples/DemoApp/App.xaml index ac7cb47..12ddef4 100644 --- a/samples/DemoApp/App.xaml +++ b/samples/DemoApp/App.xaml @@ -9,6 +9,7 @@ + diff --git a/samples/DemoApp/DemoApp.csproj b/samples/DemoApp/DemoApp.csproj index 8f9ec6e..ac5aa44 100644 --- a/samples/DemoApp/DemoApp.csproj +++ b/samples/DemoApp/DemoApp.csproj @@ -65,6 +65,9 @@ ChangeUsernamePage.xaml + + UriTestPage.xaml + CharlieTabPage.xaml @@ -90,12 +93,18 @@ + + MSBuild:Compile + MSBuild:UpdateDesignTimeXaml MSBuild:UpdateDesignTimeXaml + + MSBuild:UpdateDesignTimeXaml + MSBuild:UpdateDesignTimeXaml diff --git a/samples/DemoApp/MauiProgram.cs b/samples/DemoApp/MauiProgram.cs index 2d4413f..ef4b7b7 100644 --- a/samples/DemoApp/MauiProgram.cs +++ b/samples/DemoApp/MauiProgram.cs @@ -43,6 +43,7 @@ public static MauiAppBuilder RegisterViewModels(this MauiAppBuilder mauiAppBuild mauiAppBuilder.Services.AddTransient(); mauiAppBuilder.Services.AddTransient(); mauiAppBuilder.Services.AddTransient(); + mauiAppBuilder.Services.AddTransient(); return mauiAppBuilder; } @@ -54,6 +55,7 @@ public static MauiAppBuilder RegisterViews(this MauiAppBuilder mauiAppBuilder) mauiAppBuilder.Services.AddTransient(); mauiAppBuilder.Services.AddTransient(); mauiAppBuilder.Services.AddTransient(); + mauiAppBuilder.Services.AddTransient(); return mauiAppBuilder; } diff --git a/samples/DemoApp/Properties/Resources.Designer.cs b/samples/DemoApp/Properties/Resources.Designer.cs index 8285cab..5fb47a1 100644 --- a/samples/DemoApp/Properties/Resources.Designer.cs +++ b/samples/DemoApp/Properties/Resources.Designer.cs @@ -60,6 +60,366 @@ internal Resources() { } } + /// + /// Looks up a localized string similar to Alpha. + /// + internal static string AlphaTab_Title { + get { + return ResourceManager.GetString("AlphaTab_Title", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Beta. + /// + internal static string BetaTab_Title { + get { + return ResourceManager.GetString("BetaTab_Title", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Finish. + /// + internal static string Button_Finish { + get { + return ResourceManager.GetString("Button_Finish", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Go Back. + /// + internal static string Button_GoBack { + get { + return ResourceManager.GetString("Button_GoBack", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Login. + /// + internal static string Button_Login { + get { + return ResourceManager.GetString("Button_Login", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to OK. + /// + internal static string Button_OK { + get { + return ResourceManager.GetString("Button_OK", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Switch to Alpha. + /// + internal static string Button_SwitchToAlpha { + get { + return ResourceManager.GetString("Button_SwitchToAlpha", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Switch to Beta. + /// + internal static string Button_SwitchToBeta { + get { + return ResourceManager.GetString("Button_SwitchToBeta", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Switch to Charlie. + /// + internal static string Button_SwitchToCharlie { + get { + return ResourceManager.GetString("Button_SwitchToCharlie", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Change your username. + /// + internal static string ChangeUsername_Heading { + get { + return ResourceManager.GetString("ChangeUsername_Heading", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to This modal will return data to the previous page even if you don't use the 'Finish' button.. + /// + internal static string ChangeUsername_Instructions { + get { + return ResourceManager.GetString("ChangeUsername_Instructions", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Change username. + /// + internal static string ChangeUsername_Title { + get { + return ResourceManager.GetString("ChangeUsername_Title", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to username. + /// + internal static string ChangeUsername_UsernamePlaceholder { + get { + return ResourceManager.GetString("ChangeUsername_UsernamePlaceholder", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Charlie. + /// + internal static string CharlieTab_Title { + get { + return ResourceManager.GetString("CharlieTab_Title", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Tabbed Page. + /// + internal static string DemoTabs_Title { + get { + return ResourceManager.GetString("DemoTabs_Title", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Error. + /// + internal static string Error { + get { + return ResourceManager.GetString("Error", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Add multiple pages. + /// + internal static string Home_Button_AddMultiplePages { + get { + return ResourceManager.GetString("Home_Button_AddMultiplePages", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Change Username (animation). + /// + internal static string Home_Button_ChangeUsernameAnimation { + get { + return ResourceManager.GetString("Home_Button_ChangeUsernameAnimation", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Change Username (no animation). + /// + internal static string Home_Button_ChangeUsernameNoAnimation { + get { + return ResourceManager.GetString("Home_Button_ChangeUsernameNoAnimation", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Logout with URI syntax:. + /// + internal static string Home_Button_LogoutWithUriSyntax { + get { + return ResourceManager.GetString("Home_Button_LogoutWithUriSyntax", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Tabbed Page demo. + /// + internal static string Home_Button_TabbedPageDemo { + get { + return ResourceManager.GetString("Home_Button_TabbedPageDemo", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Modal navigation:. + /// + internal static string Home_SubHeading_ModalNavigation { + get { + return ResourceManager.GetString("Home_SubHeading_ModalNavigation", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Tabbed navigation:. + /// + internal static string Home_SubHeading_TabbedNavigation { + get { + return ResourceManager.GetString("Home_SubHeading_TabbedNavigation", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to URI navigation:. + /// + internal static string Home_SubHeading_UriNavigation { + get { + return ResourceManager.GetString("Home_SubHeading_UriNavigation", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Home. + /// + internal static string Home_Title { + get { + return ResourceManager.GetString("Home_Title", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Welcome {0}. + /// + internal static string Home_WelcomeMessage { + get { + return ResourceManager.GetString("Home_WelcomeMessage", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Burkus.Mvvm.Maui Demo App. + /// + internal static string Login_Heading { + get { + return ResourceManager.GetString("Login_Heading", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Enter any data to login to this demo app.. + /// + internal static string Login_Instructions { + get { + return ResourceManager.GetString("Login_Instructions", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to password. + /// + internal static string Login_PasswordPlaceholder { + get { + return ResourceManager.GetString("Login_PasswordPlaceholder", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Don't have an account yet? Register here.. + /// + internal static string Login_RegisterLink { + get { + return ResourceManager.GetString("Login_RegisterLink", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Login. + /// + internal static string Login_Title { + get { + return ResourceManager.GetString("Login_Title", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to username. + /// + internal static string Login_UsernamePlaceholder { + get { + return ResourceManager.GetString("Login_UsernamePlaceholder", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to You must enter a password.. + /// + internal static string Login_Validation_RequiredPassword { + get { + return ResourceManager.GetString("Login_Validation_RequiredPassword", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to You must enter a username.. + /// + internal static string Login_Validation_RequiredUsername { + get { + return ResourceManager.GetString("Login_Validation_RequiredUsername", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to You don't actually need to register. You can just make up any username and password to login! This is just a demo page for relative navigation.. + /// + internal static string Register_Instructions { + get { + return ResourceManager.GetString("Register_Instructions", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Register. + /// + internal static string Register_Title { + get { + return ResourceManager.GetString("Register_Title", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Go Back Three Times. + /// + internal static string UriTest_Button_GoBackThreeTimes { + get { + return ResourceManager.GetString("UriTest_Button_GoBackThreeTimes", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Go Back Two Times. + /// + internal static string UriTest_Button_GoBackTwoTimes { + get { + return ResourceManager.GetString("UriTest_Button_GoBackTwoTimes", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Switch to change username. + /// + internal static string UriTest_Button_SwitchToChangeUsername { + get { + return ResourceManager.GetString("UriTest_Button_SwitchToChangeUsername", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to URI Testing. + /// + internal static string UriTest_Title { + get { + return ResourceManager.GetString("UriTest_Title", resourceCulture); + } + } + /// /// Looks up a localized string similar to Cloudy weather today 🌥️. /// diff --git a/samples/DemoApp/Properties/Resources.resx b/samples/DemoApp/Properties/Resources.resx index 1a98bb7..dcf5933 100644 --- a/samples/DemoApp/Properties/Resources.resx +++ b/samples/DemoApp/Properties/Resources.resx @@ -117,6 +117,166 @@ System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + Alpha + Translate + + + Beta + Translate + + + Finish + Translate + + + Go Back + Translate + + + Login + Translate + + + OK + Translate + + + Switch to Alpha + Translate + + + Switch to Beta + Translate + + + Switch to Charlie + Translate + + + Change your username + Translate + + + This modal will return data to the previous page even if you don't use the 'Finish' button. + Translate + + + Change username + Translate + + + username + Translate + + + Charlie + Translate + + + Tabbed Page + Translate + + + Error + Translate + + + Add multiple pages + Translate + + + Change Username (animation) + Translate + + + Change Username (no animation) + Translate + + + Logout with URI syntax: + Translate + + + Tabbed Page demo + Translate + + + Modal navigation: + Translate + + + Tabbed navigation: + Translate + + + URI navigation: + Translate + + + Home + Translate + + + Welcome {0} + Translate, {0} parameter is username + + + Burkus.Mvvm.Maui Demo App + Translate + + + Enter any data to login to this demo app. + Translate + + + password + Translate + + + Don't have an account yet? Register here. + Translate + + + Login + Translate + + + username + Translate + + + You must enter a password. + Translate + + + You must enter a username. + Translate + + + You don't actually need to register. You can just make up any username and password to login! This is just a demo page for relative navigation. + Translate + + + Register + Translate + + + Go Back Three Times + Translate + + + Go Back Two Times + Translate + + + Switch to change username + Translate + + + URI Testing + Translate + Cloudy weather today 🌥️ Translate diff --git a/samples/DemoApp/Resources/Styles/DemoAppStyles.xaml b/samples/DemoApp/Resources/Styles/DemoAppStyles.xaml new file mode 100644 index 0000000..ec2ca31 --- /dev/null +++ b/samples/DemoApp/Resources/Styles/DemoAppStyles.xaml @@ -0,0 +1,31 @@ + + + + 600 + 350 + + + + diff --git a/samples/DemoApp/ViewModels/DemoTabsViewModel.cs b/samples/DemoApp/ViewModels/DemoTabsViewModel.cs index 8f73602..a2bba80 100644 --- a/samples/DemoApp/ViewModels/DemoTabsViewModel.cs +++ b/samples/DemoApp/ViewModels/DemoTabsViewModel.cs @@ -21,7 +21,7 @@ public DemoTabsViewModel( #region Commands /// - /// Navigate back to the homepage. + /// Navigate back one page. /// [RelayCommand] private async Task GoBack() diff --git a/samples/DemoApp/ViewModels/HomeViewModel.cs b/samples/DemoApp/ViewModels/HomeViewModel.cs index 8ebf387..39bf093 100644 --- a/samples/DemoApp/ViewModels/HomeViewModel.cs +++ b/samples/DemoApp/ViewModels/HomeViewModel.cs @@ -1,7 +1,6 @@ using CommunityToolkit.Mvvm.ComponentModel; using CommunityToolkit.Mvvm.Input; using DemoApp.Abstractions; -using DemoApp.Services; using DemoApp.Views; namespace DemoApp.ViewModels; @@ -90,5 +89,25 @@ private async Task GoToTabbedPageDemo() await navigationService.Push(); } + /// + /// Logout of the application. + /// + [RelayCommand] + private async Task Logout() + { + // use the navigate URI syntax to logout with an absolute URI + await navigationService.Navigate("/LoginPage"); + } + + /// + /// Add multiple pages onto the stack. + /// + [RelayCommand] + private async Task AddMultiplePages() + { + // use the navigate URI syntax to add multiple pages + await navigationService.Navigate($"{nameof(DemoTabsPage)}/{nameof(RegisterPage)}/{nameof(UriTestPage)}"); + } + #endregion Commands } diff --git a/samples/DemoApp/ViewModels/LoginViewModel.cs b/samples/DemoApp/ViewModels/LoginViewModel.cs index cc2a917..2cdb82c 100644 --- a/samples/DemoApp/ViewModels/LoginViewModel.cs +++ b/samples/DemoApp/ViewModels/LoginViewModel.cs @@ -1,5 +1,6 @@ using CommunityToolkit.Mvvm.ComponentModel; using CommunityToolkit.Mvvm.Input; +using DemoApp.Properties; using DemoApp.Views; namespace DemoApp.ViewModels; @@ -75,9 +76,9 @@ private bool IsValidLoginForm() if (string.IsNullOrEmpty(Username)) { dialogService.DisplayAlert( - "Error", - "You must enter a username.", - "OK"); + Resources.Error, + Resources.Login_Validation_RequiredUsername, + Resources.Button_OK); return false; } @@ -85,9 +86,9 @@ private bool IsValidLoginForm() if (string.IsNullOrEmpty(Password)) { dialogService.DisplayAlert( - "Error", - "You must enter a password.", - "OK"); + Resources.Error, + Resources.Login_Validation_RequiredPassword, + Resources.Button_OK); return false; } diff --git a/samples/DemoApp/ViewModels/UriTestViewModel.cs b/samples/DemoApp/ViewModels/UriTestViewModel.cs new file mode 100644 index 0000000..f9c7615 --- /dev/null +++ b/samples/DemoApp/ViewModels/UriTestViewModel.cs @@ -0,0 +1,55 @@ +using CommunityToolkit.Mvvm.Input; +using DemoApp.Views; + +namespace DemoApp.ViewModels; + +public partial class UriTestViewModel : BaseViewModel +{ + #region Constructors + + public UriTestViewModel( + INavigationService navigationService) + : base(navigationService) + { + } + + #endregion Constructors + + #region Commands + + /// + /// Go back multiple times + /// + [RelayCommand] + private async Task GoBackMultipleTimes(int backTimes) + { + var uriBuilder = new NavigationUriBuilder(); + + for (int i = 0; i < backTimes; i++) + { + uriBuilder.AddGoBackSegment(); + } + + await navigationService.Navigate(uriBuilder.Build()); + } + + /// + /// Switch to the change username page + /// + [RelayCommand] + private async Task SwitchToChangeUsername() + { + var navigationParameters = new NavigationParameters(); + navigationParameters.UseModalNavigation = true; + + var uriBuilder = new NavigationUriBuilder() + .AddGoBackSegment() + .AddGoBackSegment() + .AddGoBackSegment() + .AddSegment(navigationParameters); + + await navigationService.Navigate(uriBuilder.ToString()); + } + + #endregion Commands +} diff --git a/samples/DemoApp/Views/ChangeUsernamePage.xaml b/samples/DemoApp/Views/ChangeUsernamePage.xaml index 70db8df..66010cc 100644 --- a/samples/DemoApp/Views/ChangeUsernamePage.xaml +++ b/samples/DemoApp/Views/ChangeUsernamePage.xaml @@ -4,39 +4,36 @@ xmlns="http://schemas.microsoft.com/dotnet/2021/maui" xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml" xmlns:burkus="http://burkus.co.uk" + xmlns:properties="clr-namespace:DemoApp.Properties" xmlns:vm="clr-namespace:DemoApp.ViewModels" - Title="Change username" + Title="{x:Static properties:Resources.ChangeUsername_Title}" x:DataType="vm:ChangeUsernameViewModel" BindingContext="{burkus:ResolveBindingContext x:TypeArguments=vm:ChangeUsernameViewModel}">