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 fromDOTNET_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 createdStartup()
— 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
— implementsINotifyPropertyChanged
; can be used for view models containing observable properties.ObservableValidator
— inheritsObservableObject
and implementsINotifyDataErrorInfo
; can be used for view models containing properties with validation attributes (see Validation for more information).BaseViewModel
— inheritsObservableValidator
, implementsIDisposable
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 theDataGrid
CurrentPage
— the index of the current page (starting from 0)TotalCount
— the total number of itemsPageSize
— the number of items displayed on a single pageSortDescriptor
— stores the name of the property used for sorting and the direction of sorting; it can be bound to theDataGrid
using theDataGridSortBindingBehavior
(see Controls for more information)PaginationLabel
— a read-only property which contains information about displayed items and the total number of itemsShowPreviousPageCommand
— a command which navigates to the previous pageShowNextPageCommand
— a command which navigates to the next pageRequestedPage
— a protected property which contains the index of the page which should be retrieved by thePopulateRows()
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.