Skip to content

Latest commit

 

History

History
161 lines (116 loc) · 15.6 KB

HOW-TO-USE.md

File metadata and controls

161 lines (116 loc) · 15.6 KB

How to use UpbeatUI

The sections below provide:

  1. Guides on some of the options for initializing UpbeatUI and configuring the UpbeatStack.
  2. Details on some of the functions that the IUpbeatService provides to ViewModels.
  3. Information on how Views should be designed for best compatibility with UpbeatUI.

Initiating with IHostBuilder

To create an UpbeatUI app using IHostBuilder (the simpler method), a ConfigureUpbeatHost extension method is provided in the UpbeatUI.Extensions.Hosting Nuget package. The method takes a required delegate parameter to create a ViewModelParameters object for the main/bottom ViewModel. ConfigureUpbeatHost also takes an optional delegate parameter that provides an IHostedUpbeatBuilder to set additional options in the application, such as specifying a custom containing window or setting specific ViewModelParameters-ViewModel-View type mappings.

The below code automatically creates a default UpbeatMainWindow with an UpbeatStack as its DataContext. A delegate to create a BottomViewModel.Parameters object is supplied, which UpbeatUI will use to create a BottomViewModel and display a BottomControl as the window's main/bottom layer. (If the BottomViewModel needs access to any services or configuration objects, they can be supplied via dependency injection similarly to Controllers in ASP.NET Core.) Please see the comments in the App.xaml.cs file in the hosted sample for guidance on additional options.

<Application
    x:Class="UpbeatUIDemo.App"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    xmlns:local="clr-namespace:UpbeatUIDemo"
    Startup="HandleApplicationStartup" />
namespace UpbeatUIDemo;

public partial class App : Application
{
  private async void HandleApplicationStartup(object sender, StartupEventArgs e) =>
      await Host.CreateDefaultBuilder(await Host.CreateDefaultBuilder(e.Args))
          .ConfigureUpbeatHost(() => new BottomViewModel.Parameters())
          .Build()
          .RunAsync();
}

By default, using UpbeatUI with IHostBuilder enables automatic mapping between ViewModelParameters and ViewModels. The default namespace and naming conventions are below, but the IHostedUpbeatBuilder provides methods to set the mapping conventions manually (either from Type-to-Type or AssemblyQualifiedName-to-AssemblyQualifiedName). Specific type relationships can also be manually mapped with the IHostedUpbeatBuilder. The UpbeatStack will always check for manually specified mappings first.

Naming Convention Example Class Name
ViewModelParameters {BaseNamespace}.ViewModel.{Name}ViewModel+Parameters SampleProject.ViewModel.PopupMessageViewModel+Parameters
ViewModel {BaseNamespace}.ViewModel.{Name}ViewModel SampleProject.ViewModel.PopupMessageViewModel

Note: In the default convention, the ViewModelParameters class must be a nested class within the ViewModel class (hence, the +).

An IHostBuilder UpbeatUI applciation will create ViewModels and perform constructor dependency injection automatically using the IServiceProvider created by the IHostBuilder. UpbeatUI with dependency injection supports Scoped dependencies, where each ViewModel is an independent scope.

Initiating Manually

An UpbeatUI application can be started manually with automatic Dependency Injection and without.

Without Dependency Injection

To start an UpbeatUI application without IHostBuilder or an IServiceProvider, both the containing window and the UpbeatStack must be created, configured, and integrated manually. Mappings between ViewModelParameters and ViewModels will also need to be manually set and factory methods provided. The process is considerably more involved, so please see the comments in the App.xaml.cs file in the manual sample for a guidance.

With Dependency Injection

To start an UpbeatUI application without IHostBuilder but with an IServiceProvider for dependency injection, the containing window and the ServiceProvidedUpbeatStack must be created, configured, and integrated manually. However, mappings between ViewModelParameters and ViewModels do not need to be set manually. The process is also somewhat involved, so please see the comments in the App.xaml.cs file in the service provided sample for a guidance. Using ServiceProvidedUpbeatStack, mapping by convention is possible using the SetDefaultViewModelLocators method, or with more control using the other SetViewModelLocators methods. UpbeatUI will create ViewModels and perform constructor dependency injection automatically using the IServiceProvider provided in the constructor. UpbeatUI with dependency injection supports Scoped dependencies, where each ViewModel is an independent scope.

The IUpbeatService interface provides functionality for ViewModels to interact with their parent UpbeatStack.

Opening New ViewModels

The most often used function is to open new child ViewModels on the UpbeatStack via the OpenViewModel or OpenViewModelAsync methods.

For example: if the currently active ViewModel wanted to display a popup message to the user, then it might want to open a PopupMessageViewModel on the UpbeatStack. To do so, it would pass a PopupMessageViewModel.Parameters object with a PopupMessage string property to the OpenViewModel or OpenViewModelAsync methods:

// Note: OpenViewModelAsync returns a Task that completes when the opened ViewModel is closed
await _upbeatService.OpenViewModelAsync(
    new PopupMessageViewModel.Parameters
    {
        PopupMessage = "Message to display in popup"
    });

The UpbeatStack will create a new PopupMessageViewModel (assuming the types are correctly mapped) by passing the submitted PopupMessageViewModel.Parameters object in the constructor for initialization (and also a new unique IUpbeatService). A PopupMessageControl will displayed to the user with the new PopupMessageViewModel set as its DataContext, so a TextBox could be bound to display the PopupMessage property.

Intercepting View and Window Close Events

ViewModels that hold ongoing work that has not been saved or committed to a database might want confirm with the user before being closed. This is accomplished with the RegisterCloseCallback method on the IUpbeatService. This method accepts an okToClose delegate that the UpbeatStack will call before attempting to close the ViewModel. The delegate should return true if the ViewModel can close, and false if not. Returning false can prevent the entire application from stopping (at least in orderly shutdown situations). The okToClose delegate can be synchronous or async.

Constructing Views

With UpbeatUI, ViewModel instances within the UpbeatStack are arranged using an ItemsControl and BlurredZPanel. Thus, ViewModel instances are set as the Content property on ContentPresenters. In order for the ViewModel instances to be rendered as visual controls, there must be an associated DataTemplate with DataType property set to the ViewModel's class. As an example, the following would be a DataTemplate defined in XAML for the above ``SampleProject.ViewModel.PopupMessageViewModel` ViewModel.

# xmlns:svm="clr-namespace:HostedUpbeatUISample.ViewModel"
<DataTemplate DataType="{x:Type svm:SampleProject.ViewModel.PopupMessageViewModel}">
  ...
</DataTemplate>

These DataTemplates must be defined as Resources within the Application instance. For simple applications, they could all be defined in the App.xaml file, though they can also be defined in separate ResourceDictionary .xaml files and merged with the Application's ResourceDictionary. See the sample projects, as they use this technique.

Sizing and Positioning Views

There are several attached properties that can be used to define where the View's content is rendered within the window. These propertes must be set on the root control or panel within the DataTemplate. The properties are:

  1. WidthPercent - Defines the amount of viewable horizontal space that the content will fill.
  2. HeightPercent - Defines the amount of viewable vertical space that the content will fill.
  3. XPositionPercent - Defines the percentage point horizontally within the viewable space that the content should be centered on.
  4. YPositionPercent - Defines the percentage point vertically within the viewable space that the content should be centered on.
  5. KeepInBounds - Defines whether the content should be kept within the viewable space or not. This is only relevent if the content is not centered.

Both WidthPercent and HeightPercent accept either one or two values. One value, such as "50%", sets the content to always fill exactly half of the viewable space. Two values, such as "10% 90%", sets the content to fill its desired size, but no smaller than 10% of the viewable space and no larger than 90% of the viewable space. The default behavior when WidthPercent or HeightPercent are unset is for content to fill its desired space, but no larger than viewable space.

For example, the following will take half of the available height, betwee half and 90% of available width, be positioned dynamically based on the bound XPositionPercent and YPositionPercent properites on the ViewModel, and will be kept within bounds (i.e., it will be rendered completely within the available space rather than potentially having a portion outside).

# xmlns:svm="clr-namespace:HostedUpbeatUISample.ViewModel"
# xmlns:uv="clr-namespace:UpbeatUI.View;assembly=UpbeatUI"
<DataTemplate DataType="{x:Type svm:SampleProject.ViewModel.PopupMessageViewModel}">
    <Grid
        uv:PercentPlace.HeightPercent="50%"
        uv:PercentPlace.WidthPercent="50% 90%"
        uv:PercentPlace.KeepInBounds="True"
        uv:PercentPlace.XPositionPercent="{Binding XPosition}"
        uv:PercentPlace.YPositionPercent="{Binding YPosition}">
        ...
    </Grid>
</DataTemplate>

For all Views except the bottom layer, it is recommended to leave empty space around the sides that the user can touch/click to close the active View.

Note: All four percent properties can accept numbers in percent format ("50%") or decimal format ("0.5").

See the various .xaml files in the sample projects for examples on how to configure DataTemplates for UpbeatUI.

Passing Position Information To ViewModels

In the above example, you can notice that the XPositionPercent and YPositionPercent properties are bound to XPosition and YPosition properties on the ViewModel instance. These values can be arbitrary, but they could also be dynamic: for example if you would like a popup to be rendered directly on top of a button that opens it. To pass the relative location of that button back to the ViewModel (so it can open a popup on that location), use the PercentPositionWithinUpbeatStackConverter on the CommandParameter. For example, the following button is bound to the ShowPopupCommand command on the ViewModel:

<DataTemplate.Resources>
    <uvc:PercentPositionWithinUpbeatStackConverter x:Key="PositionConverter" />
</DataTemplate.Resources>
...
<Button
    Command="{Binding ShowPopupCommand}"
    CommandParameter="{Binding RelativeSource={RelativeSource Self}, Converter={StaticResource PositionConverter}}"
    Margin="5">Popup</Button>

PercentPositionWithinUpbeatStackConverter works by converting the input value to the Convert method (in the above case, {RelativeSource Self}; i.e, the Button control) into a Func<Point> that the Command implemenation can call to get the percentage location of the control or element that invoked the command (the Button) within the UpbeatStack's area, like so (if using the CommunityToolkit.Mvvm NuGet package).

[RelayCommand]
private void ShowPopup(Func<Point> positionProvider)
{
    _upbeatService.OpenViewModel(
        new PopupMessageViewModel.Parameters()
        {
            Message = "Popup mesage",
            Position = positionProvider.Invoke(),
        });
}