Go to content

Bulletcode.NET

MVVM

Bulletcode.NET contains two packages for building desktop applications using the MVVM architecture: the platform-independent Bulletcode.Desktop package and the Bulletcode.Desktop.Wpf package for WPF. Support for other platforms may be added in future versions. See the Introduction for more information about installing the Bulletcode.NET NuGet packages.

Application builder

The desktop application builder is similar to the web application builder which is part of ASP.NET Core. It provides support for dependency injection, configuration and logging for desktop applications. It also includes a basic executor for background services implementing IHostedService.

The following example shows the Main() method of an application using the desktop application builder:

public class Program
{
    [STAThread]
    public static void Main()
    {
        var builder = DesktopAplication.CreateBuilder();

        builder.Logging.AddDesktopLoggingProviders( builder.Environment );

        ConfigureServices( builder.Services );

        var app = builder.Build();

        app.Run();
    }
}

The configuration manager includes environment variables with the DOTNET_ prefix, and three settings files: appsettings.json, appsettings.ENV.json (where ENV is the name of the environment), and appsettings.local.json. See Configuration for more information.

The host environment is initialized with the following information:

  • ApplicationName — the name of the entry assembly.
  • ContentRootPath — the directory containing the entry assembly.
  • EnvironmentName — taken from DOTNET_ENVIRONMENT, defaults to Production.

The ContentRootFileProvider property of the host environment is not initialized.

The logging builder is initialized using the "Logging" section of the configuration. No logging providers are registered by default. The AddDesktopLoggingProviders() extension method for ILoggingBuilder registers the text file logging provider. Also, when the application is running in production mode and a relative path is configured for the text file logger, an absolute path is created using the local application data directory for the current user and the application name. See Logging for more information.

Both the IHostEnvironment and IConfiguration are added as singletons to the service provider. The services required for logging and managing options are also registered. See Dependency Injection for more information.

The application should register additional services and specify the application class and the main window class, for example:

private static void ConfigureServices( IServiceCollection services )
{
    services.UseApplication<App>();
    services.UseMainWindow<MainWindow>();
}

The App class should inherit the WPF Application class, and should contain a reference to the application’s resource dictionary. The MainWindow class should inherit the WPF Window class.

The application and main window objects are created using the service provider, so they can have dependencies. However, the recommended approach is to add any dependencies to the view models, and not directly to the application or window classes.

To create a main window with an associated view model, which will be set as its data context, use the second variant of the UseMainWindow() method, for example:

services.UseMainWindow<MainWindow, MainWindowViewModel>();

Setup services

When a service is registered using the IDesktopApplicationSetup, its methods will be automatically called by the application builder:

  • Initialize() — immediately after the service provider is created
  • Startup() — after the application and window objects are created, immediately before the main window is shown

View models

Bulletcode.NET includes three classes which can be used as base classes for view models:

  • ObservableObject — implements INotifyPropertyChanged; can be used for view models containing observable properties.
  • ObservableValidator — inherits ObservableObject and implements INotifyDataErrorInfo; can be used for view models containing properties with validation attributes (see Validation for more information).
  • BaseViewModel — inherits ObservableValidator, implements IDisposable and provides support for creating commands which wrap the view model’s methods.

Observable properties

To create an observable property, its constructor should call the SetProperty() method with a reference to the field storing the value of the property, for example:

public class MyViewModel : BaseViewModel
{
    private string _name;

    public string Name
    {
        get => _name;
        set => SetProperty( ref _name, value );
    }
}

This method automatically triggers the PropertyChanged event if the new value is different than the current one.

If the value of the property is not stored directly as the view model’s field — for example, when it’s stored as a property of another object — a getter and setter callback can be used instead:

public class MyViewModel : BaseViewModel
{
    private Model _model;

    public string Name
    {
        get => _model.Name;
        set => SetProperty( () => _model.Name, value => _model.Name = value, value );
    }
}

Validation

If the view model inherits ObservableValidator, another overload of SetProperty() method is available which has an additional parameter. When set to true, validators are automatically executed after updating the value of the property, and the ErrorsChanged event is triggered if necessary:

public class MyViewModel : BaseViewModel
{
    private string _name;

    [Required]
    [Display( Name = "Name" )]
    public string Name
    {
        get => _name;
        set => SetProperty( ref _name, value, true );
    }
}

See Validation for more information.

Dependencies

The OnPropertyChanged() method can be called to trigger the PropertyChanged event for the specified property, for example:

public class MyViewModel : BaseViewModel
{
    private decimal _side;

    public decimal Side
    {
        get => _side;
        set
        {
            if ( SetProperty( ref _side, value ) )
                OnPropertyChanged( nameof( Area ) );
        }
    }

    public decimal Area => _side * _side;
}

The SetProperty() method returns true if the property has been updated, i.e. if the new value is different than the current one.

Another solution is to use the DependsOn attribute:

public class MyViewModel : BaseViewModel
{
    private decimal _side;

    public decimal Side
    {
        get => _side;
        set => SetProperty( ref _side, value );
    }

    [DependsOn( nameof( Side ) )]
    public decimal Area => _side * _side;
}

In this case, when the Side property is updated, the PropertyChanged event is also automatically triggered for the Area property.

When using attributes, dependencies can be nested; for example, property A can depend on B, and B can depend on C. In this case, updating C would trigger the PropertyChanged event for both A and B.

Commands

If the view model inherits BaseViewModel, the GetCommand() method can be used to create commands which can be bound to UI elements, for example buttons.

The GetCommand() method accepts a callback, which is usually a method of the view model:

public class MyViewModel : BaseViewModel
{
    public ICommand SubmitCommand => GetCommand( Submit );

    private void Submit()
    {
        // TODO
    }
}

The command can then be bound to a UI element, for example:

<Button Command="{Binding SubmitCommand}">OK</Button>

The command can also take a parameter:

public class MyViewModel : BaseViewModel
{
    public ICommand SubmitCommand => GetCommand( Submit );

    private void Submit( string sender )
    {
        // TODO
    }
}

The parameter can be passed from the UI using the CommandParameter attribute. For example:

<Button Command="{Binding SubmitCommand}" CommandParameter="{Binding Sender}">OK</Button>

The GetCommand() method can take a second parameter, which is a function indicating whether the command is enabled:

public class MyViewModel : BaseViewModel
{
    public ICommand SubmitCommand => GetCommand( Submit, () => CanSubmit );

    public bool CanSubmit => _name = null;

    private void Submit()
    {
        // TODO
    }
}

The NotifyCanExecuteChanged() method should be called when the enabled state of a command is changed:

public class MyViewModel : BaseViewModel
{
    private string _name;

    public string Name
    {
        get => _name;
        set
        {
            if ( SetProperty( ref _name, value ) )
                NotifyCanExecuteChanged( nameof( SubmitCommand ) );
        }
    }
}

This method can also be called without a parameter to update the state of all commands.

Lifetime

View models should be registered as scoped services. This can be done, for example, by using the Scoped attribute (see Dependency Injection for more information):

[Scoped]
public class MyViewModel : BaseViewModel
{
}

When pages and dialogs are created using the Shell, they have their own associated service provider scopes, which means that the view models and their dependencies are automatically destroyed when the corresponding page or dialog is destroyed.

The BaseViewModel class implements the IDisposable interface and contains a virtual Dispose() method which can be overridden if the view model needs to clean up some resources:

public class MyViewModel : BaseViewModel
{
    protected override void Dispose( bool disposing )
    {
        base.Dispose( disposing );

        // TODO
    }
}

BaseGridViewModel

The BaseGridViewModel class can be used as the base of view models for views containing a DataGrid control with paging, sorting and filtering. The template parameter of this class specifies the type of grid items. It has the following properties:

  • Items — the collection of items on the current page, which should be specified as the items source for the DataGrid
  • CurrentPage — the index of the current page (starting from 0)
  • TotalCount — the total number of items
  • PageSize — the number of items displayed on a single page
  • SortDescriptor — stores the name of the property used for sorting and the direction of sorting; it can be bound to the DataGrid using the DataGridSortBindingBehavior (see Controls for more information)
  • PaginationLabel — a read-only property which contains information about displayed items and the total number of items
  • ShowPreviousPageCommand — a command which navigates to the previous page
  • ShowNextPageCommand — a command which navigates to the next page
  • RequestedPage — a protected property which contains the index of the page which should be retrieved by the PopulateRows() method

The class which inherits BaseGridViewModel must implement the abstract PopulateRows() method. This method should retrieve items from an external source, for example an API, using the RequestedPage and SortDescriptor properties. The method should update the Items, CurrentPage and TotalCount properties. Asynchronous operations should be wrapped in the ShowBusy() method of the INaviationProvider service.