Category Archives: MAUI

CommunityToolkit.Mvvm “already contains a definition for” error

The CommunuityToolkit.Mvvm includes a wonderful MVVM source generator, but in the current .NET 6.0 release I’m using, (doesn’t happen with MAUI but does with WPF) I get errors such as

  • The type ‘MyViewModel’ already contains a definition for ‘MyProperty’
  • A partial method may not have multiple defining declarations

To fix this (at least in the current version of .NET 6.x) add a global.json file to the folder with your solution file

{
  "sdk": {
    "version": "6.0.202",
    "rollForward": "disable"
  }
}

and things should work correctly – hopefully this post will be obsolete soon with the issue fixed, but for now this solves the problem.

Appium, Android and MAUI

Prerequisites

You’ll need Appium server (I’m using Appium Server UI), I installed into my Windows c:\tools\Appium folder and next (whilst not a requirement but is very useful) grab the latest version of Appium Inspector this will allows us to inspect elements within our running Android application, I unzipped this to c:\tools\AppiumInspector.

Appium Server GUI has a button to run the inspector but, unless I’m missing something, this simply sends you to the GitHub repo for the inspector, so run the inspector yourself separately.

Creating a simple app.

Create yourself a MAUI application (mine’s call MauiUIAutomationTest, so you’re see this listed in various places below, if you create your own name, remember to update in the code as well).

Like my previous Appium test samples the code is simple (I’ll recreate here). The MainPage.xaml within the ContentPage looks like this

<Grid>
   <Grid.RowDefinitions>
      <RowDefinition />
      <RowDefinition />
      <RowDefinition />
   </Grid.RowDefinitions>

   <Entry Grid.Row="0" AutomationId="input" Margin="10" Text="{Binding Input}" />
   <Button Grid.Row="1" AutomationId="copy" Margin="10" Command="{Binding ProcessCommand}" Text="Copy" />
   <Label Grid.Row="2" AutomationId="output" Margin="10" Text="{Binding Output}"/>
</Grid>

I’m using MVVM Community Toolkit and so MainPageViewModel.cs is simply this

public partial class MainPageViewModel : ObservableObject
{
    [ObservableProperty] private string input;
    [ObservableProperty] private string output;

    [RelayCommand]
    private void Process()
    {
       Output = Input;
    }
}

Creating our test code

I’ve created an NUnit test class library and added the NuGet package Appium.WebDriver (yes I know it’s an app. but that’s the NuGet package we need). If you’re creating your unit test class library from scratch you’ll need NUnit and NUnit3TestAdapter installed as dependencies as well).

Now my first unit test is, as follows

public class Tests
{
    private AndroidDriver<AndroidElement> _driver;

    [SetUp]
    public void Setup()
    {
        var driverOptions = new AppiumOptions();
        driverOptions.AddAdditionalCapability(MobileCapabilityType.PlatformName, "Android");
        driverOptions.AddAdditionalCapability(MobileCapabilityType.AutomationName, "UiAutomator2"); 
        driverOptions.AddAdditionalCapability(MobileCapabilityType.DeviceName, "Pixel_3_XL_API_29");

        _driver = new AndroidDriver<AndroidElement>(new Uri("http://localhost:4723/wd/hub"), driverOptions); 
        _driver.ActivateApp("com.companyname.mauiuiautomationtest");
    }

    [TearDown]
    public void TearDown()
    {
        _driver.Quit();
    }

    [Test]
    public void ChangeInput_ThenCopy_ExpectMatchingOutput()
    {
        var entry = _driver.FindElementById("com.companyname.mauiuiautomationtest:id/input666");
        entry.Clear();
        entry.SendKeys("Hello World");
        var button = _driver.FindElementById("com.companyname.mauiuiautomationtest:id/copy666");
        button.Click();
        var label = _driver.FindElementById("com.companyname.mauiuiautomationtest:id/output666");
        Assert.That(label.Text, Is.EqualTo("Hello World"));
    }
}

Obviously this is not perfect as we’re running the setup and tear down code after every test, but as I only have one test, I don’t care (it’s for the reader to do this correctly). Now we setup the driver options, the platformName is ofcourse Android, the deviceName relates to the device you’re using, I’m using the Android emulator for the Pixel 3 API 29, finally automationName specifies the automation engine to use, to be honest you could remove this and it’ll default the standard Appium engine but UiAutomator2 is listed as Appiums flagship engine, so let’s use that.

I had all sorts of issues trying to get Appium to start my MAUI app (as it will try to install it), however I think this is likely down to the way Visual Studio builds the apk, i.e. I probably need to publish a signed apk or the likes, but for now I’ll let Visual Studio deploy the app and then access it via Appium. Hence we have the following code to active an already installed app.

 
_driver.ActivateApp("com.companyname.mauiuiautomationtest");

Eager to see something working?

If you’re eager to see something working, then

  • Deploy your application to your Android emulator (I just get Visual Studio to deploy it)
  • Run the Appium Server GUI and start a new session
  • Run your unit test

If all went well the test was green and if not, check your application name etc.

Appium Inspector

The Appium server does not come with an inspector now, you need to download it separately (as stated earlier). Ofcourse we can probably get a lot done without an inspector, but it helps and gives confidence that if it see’s our id’s etc. then Appium will. You’ll need to tell the inspector to connect to the Appium server – make sure that Remote Path has the URL /wd/hub and you have valid the following Desired Capabilities

{
  "appium:automationName": "UIAutomator2",
  "appium:deviceName": "Pixel_3_XL_API_29",
  "platformName": "Android"
}

Ensure your have Appium server is running. Before you run the inspector just make sure your app. is running (just makes things simpler) and now start a session from your Appium Inspector. It should show you the screen of the current running application.

As you can see from the code below, we expect a “copy” automation id

<Button Grid.Row="1" AutomationId="copy" Margin="10" Command="{Binding ProcessCommand}" Text="Copy" />

MAUI AutomationId’s do not appear as AutomationId or Accessibility Id for that matter, but instead they become id’s but prefixed with you app name, i.e. that’s why our unit tests has com.companyname.mauiuiautomationtest:id/output. All that really matters is that we have a unique value on an Appium accessible property to do some UI Automation testing. We do have the option of XPath locators, but these should probably be used as a the last resort, if we do not have anything else to locate against.

Creating a drawable control in MAUI

MAUI includes relatively primitive graphics drawing options for the developer. Your class can implement the IDrawable interface and be assigned to a GraphicsView visual element.

So for example, let’s implement a barebones IDrawable

internal class MyDrawable : IDrawable
{
    public void Draw(ICanvas canvas, RectF dirtyRect)
    {
        canvas.DrawRectangle(1, 1, dirtyRect.Width, dirtyRect.Height);
    }
}

Obviously this does very little of interest except draws a rectangle in the view area that the code which hosts the drawable take up.

So to display this in our XAML we declare the drawable as a resource, like this

<ContentPage.Resources>
   <drawables:MyDrawable x:Key="MyDrawable" />
</ContentPage.Resources>

Finally we assign this drawable to a Graphics views which hosts the drawable like this

<GraphicsView Drawable="{StaticResource MyDrawable}"
   HeightRequest="120"
   WidthRequest="400" />

The ICanvas used within the IDrawable offers a lot of methods to draw lines, rectangles, text etc.

Note: It reminds me of the Canvas in Delphi all those many years back.

Ofcourse, we might prefer to remove the requirement of using resources and the GraphicsView. We’re not really removing them so much as just creating our own GraphicsView, so for example let’s create our control like this

public class MyGraphics : GraphicsView
{
    public MyGraphics()
    {
        Drawable = new MyDrawable();
    }
}

and now we can just use like this

<MyGraphics 
   HeightRequest="120"
   WidthRequest="400" />

Dependency injection using Shell in MAUI

The MauiAppBuilder (as seen in the MauiProgram class) i.e.

var builder = MauiApp.CreateBuilder();

exposes the property Services which is a collection of ServiceDescriptor objects. This is where we register our “services”. We can use AddSingleton or AddTransient or AddScoped.

So for example

builder.Services.AddSingleton<MainPage>();
builder.Services.AddSingleton<MainPageViewModel>();
  • AddSingleton
    As the name suggests, a single instance of a type is created across the lifetime of the application.
  • AddTransient
    A new instance of a type is created for each request, the equivalent of creating a new instance for every type that has a dependency, hence each gets a unique instance.
  • AddScoped
    A new instance of a type is created for a request and all dependencies within that request gets the same instance. For different requests a new instance is created, hence unique across requests.

Whilst the IoC container with MAUI Shell, isn’t as comprehensive in features as some containers, such as Autofac, it’s good enough for many. It does include constructor dependency injection so we will probably fulfil most use cases.

Okay, let’s assume with have a MainPage and on that we have a button which displays an AboutPage. As per the code above suggests. We can create a view model for each page, so we have a MainPageViewModel and AboutPageViewModel, then within the actual .xaml.cs of those pages, in the constructor we have code such as

public MainPage(MainPageViewModel vm)
{
   InitializeComponent();
   BindingContext = vm;
}


public AboutPage(AboutPageViewModel vm)
{
   InitializeComponent();
   BindingContext = vm;
}

Now all we need to do is register the pages and view models within the MauiProgram with the builder, so we have

builder.Services.AddSingleton<MainPage>();
builder.Services.AddSingleton<MainPageViewModel>();
builder.Services.AddTransient<AboutPage>();
builder.Services.AddTransient<AboutPageViewModel>();

When Maui starts up and loads MainPage.xaml it will inject the MainPageViewModel and also when we navigate to our AboutPage using something like the code below, again the view model is injected

[RelayCommand]
private async Task About() => await AppShell.Current.GoToAsync(nameof(AboutPage));

Note: The example uses the MVVM Community Toolkit, hence the RelayCommandAttribute.

We may need to resolve dependencies ourselves (in other words we’re unable to use the magic of something like constructor injection). Rather strangely I would have expected MAUI to already have code within for this, but it seems that current the solution is to create (or use an existing) ServiceProvider class. For example, the one shown below is taken from David Ortinau’s sample.

public static class ServiceProvider
{
   public static TService GetService<TService>()
      => Current.GetService<TService>();

   public static IServiceProvider Current
      =>
#if WINDOWS10_0_17763_0_OR_GREATER
	MauiWinUIApplication.Current.Services;
#elif ANDROID
        MauiApplication.Current.Services;
#elif IOS || MACCATALYST
	MauiUIApplicationDelegate.Current.Services;
#else
	null;
#endif
}

Now now we can use the static class ServiceProvider to get the Services, for example let’s remove the dependency injection in the AboutPage constructor and instead use the ServiceProvider like this

BindingContext = ServiceProvider.GetService<AboutPageViewModel>();

Ofcourse this is not so easily testable, but the option is there if required.

Navigating to different pages using MAUI and Shell

By default MAUI includes the Shell (see AppShell.xaml/AppShell.xaml.cs). We can use method on this object to navigate to pages in MAUI, for example if we add a simple AboutPage and want to navigate to it from a our view model we can write something like this

[RelayCommand]
private async Task About() => await AppShell.Current.GoToAsync(nameof(AboutPage));

Obviously the nameof(AboutPage) maps the the name we’ll be registering for the AboutPage.

Now we need to register the page within the shell, so go to AppShell.xaml.cs and add the following

Routing.RegisterRoute(nameof(AboutPage), typeof(AboutPage));

That’s all we need to do.

MVVM with the MVVM Community Toolkit

When you first create a MAUI application, you might be surprised to find there’s no view models setup by default. I guess this just allows us to see a bare bones application without any preference for MVVM libraries etc.

Let’s try out the MVVM community toolkit with the template generated MAUI app. This toolkit which uses code generators to allow us to create really simple look view model code without all the ceremony…

  • Add the nuget package CommunityToolkit.Mvvm to your references
  • I’m going to store my view models within the ViewModels folder, so add a new folder to your project named ViewModels
  • We’re going to replace the code within MainPage.xaml.cs with our view model, so add a file to ViewModels named MainPageViewModel.cs and it should look like this to start us off
    using CommunityToolkit.Mvvm.ComponentModel;
    
    namespace MauiAppTest.ViewModels;
    
    public partial class MainPageViewModel : ObservableObject
    {
    }
    

    Note: It’s important to make this a partial class, we’ll see why later. The ObservableObject simply gives us the INotifyPropertyChanged and INotifyPropertyChanging implementations.

  • Now within MainPage.xaml.cs, delete the OnCounterClicked method and also delete the line int count = 0;
  • To MainPage.xaml.cs, within the constructor add
    BindingContext = new MainPageViewModel();
    
  • Finally, for now, go to MainPage.xaml and delete the Button’s XAML
    Clicked="OnCounterClicked"
    

At this point we’ve got a bare bones view model which does nothing, so first off we’ll need a property for the count changes and a command which is called to change the count.

  • Within the MainPageViewModel.cs add the following which will create an observable property
    [ObservableProperty] 
    int count;
    
  • Within the MainPage.xaml, change the button’s text to bind to our Count property
    Text="{Binding Count, StringFormat='Click me {0}'}"
    

You might be wondering where the Count property came from, because in our MainPageViewModel.cs with simply declare a field named count (all lowercase and not a property). Well this is the magic of the MVVM toolkit. If you expand the MainPageViewModel.cs in solution explorer you’ll see the class and expanding that you’ll see a Count property, OnCountChanged and OnCountChanging methods where did these come from?

Originally, when we created our view model class, the first important thing is that the class must be a partial class. This allows the next bit of magic, the source generator, to add code to our class without (obviously) altering our class.

When we declare a field using the ObservablePropertyAttribute the MVVM source generator will create the property (with the expected case) along with methods that we could implement for OnXXXChanging and OnXXXChanged. For now let’s look to complete the functionality required to bind the Button to a command and change the Count.

  • Like the ObservableProperty we have another MVVM magical attribute, so add the following code to the MainPageViewModel class
    // you'll need using CommunityToolkit.Mvvm.Input;
    
    [RelayCommand]
    void IncrementCount()
    {
      Count++;
    }
    
  • Now within MainPage.xaml, add the following to the Button XAML
    Command="{Binding IncrementCountCommand}"
    

    Note how the method name is used suffixed with Command when the generator creates our code.

If you try this out, you should see that clicking the button in the UI increments the counter and displays this all via MVVM binding.

We can add code to tell the UI via binding whether the command can be executed by using one of the RelayCommandAttribute’s parameters, CanExecute. Using this we can tell RelayCommand the name of the method to execute, which should return a bool to tell the binding whether we CanExecute the command or now, for example lets add the following to the MainPageViewModel

[ObservableProperty]
bool canIncrement;

bool CanExecuteIncrement() => canIncrement;

and change the existing RelayCommand to

[RelayCommand(CanExecute = nameof(CanExecuteIncrement))]

If you run this now you’ll find that the initial binding of IncrementCountCommand will the CanExecuteIncrement method call returning false and the button will ofcourse be disabled, and we can not enable it at the moment – so let’s just add a simple Checkbox to our MAUI app that binds to CanIncrement, so add before the Button

<CheckBox IsChecked="{Binding CanIncrement}" />

Whilst this will change/toggle the CanIncrement property it will not (as is) tell the binding to essentially re-run the CanExecuteIncrement method. For this to work we just add another attribute to the canIncrement field, making it look like this

[ObservableProperty]
[NotifyCanExecuteChangedFor(nameof(IncrementCountCommand))]
bool canIncrement;

The addition of NotifyCanExecuteChangedFor will then call IncrementCountCommand.NotifyCanExecuteChanged() and update the UI accordingly.

Earlier we mentioned that code for the property generates extra methods such as OnXXXChanged. Let’s implement our own OnCanIncrementChanged method. These methods are partial so if we add the following to the MainPageViewModel

partial void OnCanIncrementChanged(bool value)
{
  Debug.WriteLine("OnCanIncrementChanged called");
}

Now, when the property CanIncrement changes, this method is called with the current property value passed as a parameter.

References

MVVM source generators
ObservableProperty attribute
RelayCommand attribute

Adding fonts the a MAUI application

When you create a MAUI application, the default application’s MauiProgram.cs contains the following

builder
  .UseMauiApp<App>()
  .ConfigureFonts(fonts =>
  {
    fonts.AddFont("OpenSans-Regular.ttf", "OpenSansRegular");
    fonts.AddFont("OpenSans-Semibold.ttf", "OpenSansSemibold");
  });

As can be seen, we’re using .AddFont to add .ttf filename along with an optional alias.

The .ttf files are stored in the Resources/Fonts folder or our application’s project and ofcourse the file name’s match those used in .AddFont

To use our font’s within the MAUI XAML we simply use the alias name like this

<Setter Property="FontFamily" Value="OpenSansRegular"/>

This is taken from the default MAUI application’s Styles.xaml file.

Adding our own font

This all looks fairly simple, so let’s add a font from Google Fonts.

I’m going to download the DynaPuff font, as I want something that will stand out when its changed.

  • Download your font choice
  • From the zip I opened static\DynaPuff and copied DynaPuff-Regular.ttf into my application’s Resources/Fonts folder
  • Within MauiProgram.cs I added the following to the ConfigureFonts method
    fonts.AddFont("DynaPuff-Regular.ttf", "DynaPuff");
    
  • To test this, I changed a XAML Label element (the Hello, World label in the default generated application) to add this
    FontFamily="DynaPuff"
    

And that’s it, really simple.