New methods to override for ViewModelBase in TinyMvvm

In the latest preview of TinyMvvm, 4.0.1-pre4, there are new methods added to the ViewModelBase class. The new methods will make it easier to handle App lifecycle events in a ViewModel. For example, if you want to unsubscribe from an event when the app goes to sleep. Before you had to handle this in the Application class.

Following methods has been added for you to override:

  • Task OnApplicationResume();
  • Task OnApplicationSleep();

Those new features are available for both TinyMvvm.Forms (for Xamarin.Forms) and TinyMvvm.Maui (for .NET MAUI). To make them work you need to change the base class of your App class to TinyApplication instead of Application.

If you don't want to change to TinyApplication, nothing old will be broken, but the new methods will not work.

The code for this release is currently just in the maui branch, https://github.com/TinyStuff/TinyMvvm/tree/4.0.1-pre4

TinyMvvm for .NET MAUI – Preview

TinyMvvm is an open-source project I started back in 2017 primary to be used in my own apps, both private projects, and client projects. I recently released 3.0 that has been in preview for a long time and I used it in apps I have shipped to the app stores. After the 3.0 release, I have focused on bringing TinyMvvm to .NET MAUI and now there is a public preview available.

Instead of migrating the TinyMvvm.Forms project to .NET MAUI I created a new .NET 6 project with the name TinyMvvm.Maui. That means that both will live side-by-side The first thing I did there was to copy all the code from the Forms project and migrate it to MAUI, .NET 6, and C#10. That means that I am using the MAUI namespaces instead of the Xamarin.Forms namespaced, I am using global usings and file-scoped namespaces.

The big change compared to the Forms version of TinyMvvm is how you initialized it. The only thing you need to do now is to use the extension method UseTinyMvvm() on the MauiAppBuilder and initialize the resolver as shown below. If your ViewModels or View are in another assembly you can specify it as an argument to UseTinyMvvm(Assembly viewAssembly, Assembly, viewModelAssembly). If you don't use Shell navigation you can pass an argument to use classic navigation. UseTinyMvvm(bool useClassicNavigation).

The next step is to analyze how I can optimize TinyMvvm for .NET MAUI.

public static MauiApp CreateMauiApp()
{
            var builder = MauiApp.CreateBuilder();
            builder
                .UseMauiApp<App>()
                .ConfigureFonts(fonts =>
                {
                    fonts.AddFont("OpenSans-Regular.ttf", "OpenSansRegular");
                })
                .UseTinyMvvm();

            var app = builder.Build();

            Resolver.SetResolver(new ServiceProviderResolver(app.Services));

            return app;
}

You can install the package from NuGet.

To find out more about how to use TinyMvvm you can read the documentation for Xamarin.Forms. Before the stable release of this package, the documentation will be fully updated with how to use it with .NET MAUI.

If you have feedback please create an issue on GitHub.

GitHub Actions and Cake for TinyMvvm

To build and publish TinyMvvm I have used Azure Pipelines and a classic build pipeline together with a release pipeline. For the upcoming 3.0 release (in preview right now) I have migrated the pipeline to GitHub Actions. There are several reasons for that:

  • I want to make the CI/CD open so all can see what happend. You can have an open project in Azure DevOps as well, but the project we use for building TinyStuff libraries are to old to make public and I felt that I it was better to use the time to migrate to GitHub Actions according to the other reasons.

  • I want to have everything together, GitHub Actions is according to me better integrated in GitHub.

  • I feel that GitHub Actions is the future, both Azure DevOps and GitHub is Microsoft products and what I can see, Microsoft have mor focus on GitHub Actions.

  • GitHub feels like the natural place for Open Source.

To define the build I decided to use Cake and the reasons for that is:

  • Cake is cross-service. So if I want to move it to an other service I can resuse the most parts. Many services using yaml, but with different flawors of it, so it cannot be reused, not even between the both Microsoft controlled products GitHub Action and Azure Pipelines. The Cake scripts will be easy to use in all major services.
  • It is C# and I like to be able to use C# for defining the build and publish steps.
  • It is Open Source and easy to extend.

The only downside for Cake is that thera are not much good documentation about how to use it. There are good API-documentation, but not so much "How to"-documentation. But if you know how the underlying processes works it will be pretty easy to get started anyway.

GitHub Actions

I have defined two actions so far for TinyMvvm, one for when codes is pushed and one for when a released is published.

Push

Everytime code is pushed to either the master branch or to a pull request the code is built to verify that noting is broken.

You can find details about the action here:
https://github.com/TinyStuff/TinyMvvm/blob/3.0/.github/workflows/main.yml

Release

When I manually create a release in GitHub this action will trigger, the name I will give the tag will be used for the version number for tha package.

The Release action will build the code from the tag and it will publish the packages to NuGet.

You can find details about the action here:
https://github.com/TinyStuff/TinyMvvm/blob/3.0/.github/workflows/release.yml

Cake

The cake script has three tasks defined; Build, Pack and Publish. The build action, only runs the build task. The Release actions is running all tasks.

TinyMvvm 2.4.1

Today, I released a new version of TinyMvvm, 2.4.1.

The release contains this:

  • Added ShellViewBase to optimize how ViewModels are created. Before the ViewModel was created during OnAppearing when using Shell navigation. It also ran Initialize during OnAppearing. But there are cases where you want it to run earlier, so I added a new class that you can use to achieve that. You can also pass trueto the constructor of ViewBase to achieve the same thing.

    In XAML and ShellViewBase:

    <mvvm:ShellViewBase
    xmlns:mvvm="clr-namespace:TinyMvvm.Forms;assembly=TinyMvvm.Forms"
    xmlns:vm="clr-namespace:SampleApp.ViewModels"
    xmlns="http://xamarin.com/schemas/2014/forms"
    xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
    x:Class="SampleApp.Views.MainView" 
    x:TypeArguments="vm:MainViewModel">
    
    </mvvm:ViewBase>

    With a parameter to the constructor of ViewBase:

    public partial class MainView
    {
    public MainView() : base(true)
    }
    <mvvm:ViewBase
    xmlns:mvvm="clr-namespace:TinyMvvm.Forms;assembly=TinyMvvm.Forms"
    xmlns:vm="clr-namespace:SampleApp.ViewModels"
    xmlns="http://xamarin.com/schemas/2014/forms"
    xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
    x:Class="SampleApp.Views.MainView" 
    x:TypeArguments="vm:MainViewModel">
    
    </mvvm:ViewBase>
  • Removed the need of TinyMvvm.Initialize(); It now marked as obsolete, so it will not break your code. The big win by removing the need for it is that it is easier to get started with TinyMvvm.

  • Refactored so that ViewModelBase implements the new IViewModelBase interface. The ViewBase is refactored so IViewModelBase is used instead of ViewModelBase. This is a request from a developer that uses TinyMvvm and it makes the library more flexible.

Read more

If you want to read more about TinyMvvm, I recommend the Get Started Tutorial, and also these blog posts:

Contribute

The full source code and documentation can be found on GitHub. Feedback and contributions are very welcome.

Improvments to TinyInsights

Today I released TinyInsights 1.1.7 with some improvements.

I have removed so it will not await when sending events to providers anymore. The reason is that it affected one of my apps in a negative way when I was tracking page views in the OnAppearing (when using Xamarin.Forms) method for a view. Also, users experienced navigation to be very slow. When I wrapped the call in Task.Run it worked like a charm again, so now I decided to build-in that behavior into TinyInsight.

The second change I have done is that I had wrapped all calls to external providers in a try/catch statement, except for methods that tracking error. This because that if a call failed it maybe can be tracked if you are using another provider for tracking error than the one that failed.

And I also want to say thank you to JTOne123 for a PR that enables SourceLink.

Contribute

The full source code and documentation for TinyInsights can be found on GitHub. Feedback and contributions are very welcome.

Autofac vs TinyIoC

I recently added a resolver that is powered by TinyIoC to TinyMvvm because I got a request to do so because they said that performance was better. I have used Autofac for many years and I have never had any problems with it. But I decided to replace Autofac with TinyIoC in one app that I am working with right now, just to try to measure the difference.

What I did is that I logged 4 runs of the initializing code of the app for each IoC provider. I used the iOS simulator in debug mode. Here is the result:

Autofac (ms) TinyIoC (ms)
136 16
91 14
83 13
91 14

I was surprised that the difference was so big, we will probably have other numbers when running it in release mode, but I think that this test gives us a pretty good indication of what the performance is. I will definitely continue with TinyIoC in this app and I will also use it for new apps.

TinyIoC for TinyMvvm

There was an issue created for TinyMvvm regarding to use TinyIoC as the IoC-container. I had created an implementation of IResolver for Autofac. But now I decided to create one for TinyIoC too.

The implementation for it can be installed via the NuGet package, TinyMvvm.TinyIoC, https://www.nuget.org/packages/TinyMvvm.TinyIoC.

The code for setting up the container and the Resolver for the Sample project will look like this if you want to use TinyIoC:

TinyIoCContainer.Current.Register<INavigationHelper>(navigationHelper);

foreach (var type in appAssembly.ExportedTypes.Where(x => x.IsSubclassOf(typeof(ViewModelBase))))
{
    TinyIoCContainer.Current.Register(type);
}

foreach (var type in appAssembly.ExportedTypes.Where(x => x.IsSubclassOf(typeof(Page))))
{
    TinyIoCContainer.Current.Register(type);
}

Resolver.SetResolver(new TinyIoCResolver());

Snippets for TinyMvvm

This blog post is a part of the Xamarin Month, a great initiative by Luis Matos. The theme for this Xamarin Month is code snippets. Here is my contribution to the Xamarin Month.

TinyMvvm is a library that I created to make me more productive when I developing apps with Xamarin.Forms.

Snippets

All snippets are available in the repository for TinyMvvm.

TinyCommand (tmcmd)

TinyMvvm has its own implementation of ICommand. You can use Xamarin.Forms Command, but if you use the TinyCommand you don't have to add references to Xamarin.Forms in your ViewModels.

Shortcut: tmcmd

private ICommand? command;
public ICommand Command => command ??= new TinyCommand(async() => {

}

TinyCommand<T> (tmcmdt)

TinyCommand<T> is a generic TinyCommand that is able to handle a parameter.

Shortcut: tmcmdt

private ICommand? command;
public ICommand Command => command ??= new TinyCommand<object>(async(parameter) => {

}

TinyMvvm property (tmprop)

I often use PropertyChanged.Fody so I don't need this snippet, but if you don't use it, this snippet can be very useful.

Shortcut: tmprop

private string propertyName;
public string PropertyName
{
    get => propertyName;
    set => Set(ref propertyName, value);
}

Override Initialize (tminit)

In the ViewModel you can override Initialize if you have ViewModelBase as your base class. It will run when the ViewModel is created.

Shortcut: tminit

public async override Task Initialize()
{
    await base.Initialize();
}

Override OnAppearing (tmapp)

In the ViewModel you can override OnAppearing if you have ViewModelBase as your base class. It will be triggered by when the View is appearing.

Shortcut: tmapp

public async override Task OnAppearing()
{
    await base.OnAppearing();
}

Override OnDisappearing (tmdis)

In the ViewModel you can override OnDisappearing if you have ViewModelBase as your base class. It will be triggered by when the View is disappearing.

Shortcut: tmdis

public async override Task OnDisappearing()
{
    await base.OnDisappearing();
}

IsBusy (IsBusy)

In the ViewModelBase there is a property with the name IsBusy. I use to set it to true when I loading data so I can bind an ActivityIndicator to it. There is also a property with the name IsNotBusy that always will have the opposite value of IsBusy. This snippet can be used to surround your code with IsBusy.

Shortcut: IsBusy

IsBusy = true;
//Your code
IsBusu = false;

Import snippets

For Visual Studio on Windows, snippets are imported with the Code Snippets Manager that you will find in the Tools menu. Read more here, https://docs.microsoft.com/en-us/visualstudio/ide/walkthrough-creating-a-code-snippet?view=vs-2019#import-a-code-snippet

For Visual Studio for Mac, add them in the ~/Library/VisualStudio/8.0/Snippets folder.

Read more

If you want to read more about TinyMvvm, I recommend the Get Started Tutorial, and also these blog posts:

Contribute

The full source code and documentation for TinyMvvm can be found on GitHub. Feedback and contributions are very welcome.

Get started with TinyMvvm

This is a tutorial that will guide you through how to get started building an app using Xamarin.Forms and TinyMvvm.

  1. Create a new project based on the template for a blank Xamarin.Forms app. In this example, the name of the project will be SampleApp. The template will generate one project for shared code and one project per target platform.

  2. Install the following NuGet packages into all projects:

    • TinyMvvm.Forms
    • TinyMvvm.Autofac
  3. Create a new ContentPage XAML in the SampleApp project and give it the name AppShell. And add the following content to it.

    <Shell xmlns="http://xamarin.com/schemas/2014/forms" xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml" xmlns:views="clr-namespace:SampleApp.Views" x:Class="SampleApp.AppShell">
        <TabBar>
            <ShellContent Title="Home" Route="home" >
                <views:MainView />
             </ShellContent>
            <ShellContent Title="About" Route="about">
                <views:AboutView />
             </ShellContent>
        </TabBar>
    </Shell>
  4. Remove the base class in the AppShell.xaml.cs so it will look like this:

    public partial class AppShell
    {
        public AppShell()
        {
            InitializeComponent();
        }
    }
  5. Navigate to App.xaml.cs and set MainPage to AppShell. You can also delete the MainPage.xaml- and MainPage.xaml.cs files.

    public partial class App : Application
    {
        public App()
        {
            InitializeComponent();
    
            MainPage = new AppShell();
        }
    }
  6. On the rows before you set MainPage to AppShell write the initialization code for TinyMvvm:

    • Use ShellNavigationHelper and register all views in the current Assembly. This register all views so we can use the class name as the key if we use the classic (non-Shell) navigation or we can use ViewModelNavigation that is powered by the Shell navigation.
    • The sample is using Autofac as it's IoC container, but you can use whatever container you want. The only thing you need to do to use another is to create an implementation of IResolver that uses it. Register all classes that is a subType of Page (Xamarin.Forms) and ViewModelBase (TinyMvvm).
    • Register the container to the Resolver, the Resolver is used internally by TinyMvvm, but you can also use it in your code.
    • The last thing to do is to call the *Initialize method for TinyMvvm.

      public App()
      {
          InitializeComponent();
      
          var navigationHelper = new ShellNavigationHelper();
      
          var currentAssembly = Assembly.GetExecutingAssembly();
          navigationHelper.RegisterViewsInAssembly(currentAssembly);
      
          var containerBuilder = new ContainerBuilder();
      
          containerBuilder.RegisterInstance<INavigationHelper>(navigationHelper);
      
          var appAssembly = typeof(App).GetTypeInfo().Assembly;
          containerBuilder.RegisterAssemblyTypes(appAssembly)
                 .Where(x => x.IsSubclassOf(typeof(Page)));
      
          containerBuilder.RegisterAssemblyTypes(appAssembly)
                 .Where(x => x.IsSubclassOf(typeof(ViewModelBase)));
      
          var container = containerBuilder.Build();
      
          Resolver.SetResolver(new AutofacResolver(container));
      
          TinyMvvm.Forms.TinyMvvm.Initialize();
      
          MainPage = new AppShell();
      }
  7. Create a new folder called ViewModels in the SampleApp project.

  8. Create two classes in the ViewModels folder, MainViewModel and AboutViewModel.

  9. Add ViewModelBase as the base class for MainViewModel and AboutViewModel:

    public class MainViewModel : ViewModelBase
    {
        public MainViewModel()
        {
        }
    }
    public class AboutViewModel : ViewModelBase
    {
        public AboutViewModel()
        {
        }
    }
  10. Edit MainView.xaml and AboutView.xaml to have ViewBase as it's base class. To set BindingContext to the the ViewModel, use the x:TypeArguments for the View.

    <mvvm:ViewBase
    xmlns:mvvm="clr-namespace:TinyMvvm.Forms;assembly=TinyMvvm.Forms"
    xmlns:vm="clr-namespace:SampleApp.ViewModels"
    xmlns="http://xamarin.com/schemas/2014/forms"
    xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
    x:Class="SampleApp.Views.MainView" 
    x:TypeArguments="vm:MainViewModel">
    
    </mvvm:ViewBase>
    <mvvm:ViewBase
    xmlns:mvvm="clr-namespace:TinyMvvm.Forms;assembly=TinyMvvm.Forms"
    xmlns:vm="clr-namespace:SampleApp.ViewModels"
    xmlns="http://xamarin.com/schemas/2014/forms"
    xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
    x:Class="SampleApp.Views.AboutView" 
    x:TypeArguments="vm:AboutViewModel">
    
    </mvvm:ViewBase>
  11. Remove the base class from MainView.xaml.cs and AboutView.xaml.cs. Because is it a partial class you don't have to specify it both in the XAML-file and in the code-behind file.

  12. Create a new View, DetailsView, and a new ViewModel, DetailsViewModel as in the same style as above.

  13. In MainViewModel, add the following below. The override of Initialize is running when BindingContext for the view has been set. You can also override OnAppearing to run code when the view will appear and OnDisappearing to run code when the view will disappear. IsBusy can be used to bind an ActivityIndicator to. For Commands, use TinyCommand instead of Xamarin.Forms.Command to keep the ViewModel clean from Xamarin.Forms references. To navigate, use the Navigation property from ViewModelBase. Here you can use the name of a ViewModel as a part of a URL to navigate to a route. With TinyMvvm you can also specify a parameter to pass to the target ViewModel.

    public class MainViewModel : ViewModelBase
    {
        private ObservableCollection<string> names;
        public ObservableCollection<string> Names
        {
            get => names;
            set => Set(ref names, value);
        }
    
        public async override Task Initialize()
        {
            IsBusy = true;            
            await base.Initialize();
    
            Names = new ObservableCollection<string>(new List<string>()
            {
                "Daniel",
                "Ella",
                "Willner"
            });
    
            IsBusy = false;
        }
    
        public override Task OnAppearing()
        {
            return base.OnAppearing();
        }
    
        public override Task OnDisappearing()
        {
            return base.OnDisappearing();
        }
    
        private ICommand details;
        private ICommand Details => details ??= new TinyCommand<string>(async(name) =>
        {
            await Navigation.NavigateToAsync($"{nameof(DetailsViewModel)}?name={name}", DateTimeOffset.Now);
        });
    }
  14. To MainView.xaml add the following code to show the bind a CollectionView to the data in the ViewModel:

     <Grid>
        <ActivityIndicator HorizontalOptions="Center" VerticalOptions="Center" IsRunning="False" IsVisible="{Binding IsBusy}" />
        <CollectionView x:Name="Names" ItemsSource="{Binding Names}" IsVisible="{Binding IsNotBusy}">
            <CollectionView.ItemTemplate>
                <DataTemplate>
                    <ContentView Padding=" 10">
                        <ContentView.GestureRecognizers>
                            <TapGestureRecognizer Command="{Binding Source={x:Reference Names}, Path=BindingContext.Details}" CommandParameter="{Binding}" />
                        </ContentView.GestureRecognizers>
                        <Label Text="{Binding }" />
                    </ContentView>
                </DataTemplate>
            </CollectionView.ItemTemplate>
        </CollectionView>
    </Grid>
  15. Go to DetailsViewModel.cs and add the code below to receive the parameters sent to it. QueryParameters will be a Dictionary<string, string> and contains the query parameters specified in the navigation URL. If you also specified a parameter you can access it via the NavigationParameter property.

    
    public class DetailsViewModel : ViewModelBase
    {
        public async override Task Initialize()
        {
            await base.Initialize();
    
            Name = QueryParameters["name"];
            var dateParameter = (DateTimeOffset)NavigationParameter;
    
            Date = dateParameter.ToString();
        }
    
        private string name;
        public string Name
        {
            get => name;
            set => Set(ref name, value);
        }
    
        private string date;
        public string Date
        {
            get => date;
            set => Set(ref date, value);
        }
    }
  16. In the DetailsView.cs add the following code to show the data passed from MainViewModel:

    <mvvm:ViewBase
        xmlns:mvvm="clr-namespace:TinyMvvm.Forms;assembly=TinyMvvm.Forms"
        xmlns:vm="clr-namespace:SampleApp.ViewModels"
        xmlns="http://xamarin.com/schemas/2014/forms"
        xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
        x:Class="SampleApp.Views.DetailsView" x:TypeArguments="vm:DetailsViewModel">
        <StackLayout Padding="10">
            <Label Text="{Binding Name}" />
            <Label Text="{Binding Date}" />
        </StackLayout>
    </mvvm:ViewBase>
  17. Navigate to AboutViewModel.cs and add the following code to use with a button to navigate to the MainView using the route specified in the Shell.

    public class AboutViewModel : ViewModelBase
    {
        private ICommand home;
        public ICommand Home => home ?? new TinyCommand(async () =>
        {
            await Navigation.NavigateToAsync("//home");
        });
    }
  18. Go to AboutView.xaml and create a Button to use with the Command in AboutViewModel.

    <mvvm:ViewBase
        xmlns:mvvm="clr-namespace:TinyMvvm.Forms;assembly=TinyMvvm.Forms"
        xmlns:vm="clr-namespace:SampleApp.ViewModels"
        xmlns="http://xamarin.com/schemas/2014/forms"
        xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
        x:Class="SampleApp.Views.AboutView" x:TypeArguments="vm:AboutViewModel">
     <Grid>
         <Button Text="Go home!" Command="{Binding Home}" />
     </Grid>
    </mvvm:ViewBase>

The complete code for this sample can be found here: https://github.com/TinyStuff/TinyMvvm/tree/master/src/Samples