The sections below provide:
- Guides on some of the options for initializing UpbeatUI and configuring the
UpbeatStack
. - Details on some of the functions that the
IUpbeatService
provides to ViewModels. - Information on how Views should be designed for best compatibility with UpbeatUI.
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.
An UpbeatUI application can be started manually with automatic Dependency Injection and without.
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.
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.
Using IUpbeatService
The IUpbeatService
interface provides functionality for ViewModels to interact with their parent UpbeatStack
.
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.
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.
With UpbeatUI, ViewModel instances within the UpbeatStack
are arranged using an ItemsControl
and BlurredZPanel
. Thus, ViewModel instances are set as the Content
property on ContentPresenter
s. 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 DataTemplate
s must be defined as Resource
s 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.
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:
WidthPercent
- Defines the amount of viewable horizontal space that the content will fill.HeightPercent
- Defines the amount of viewable vertical space that the content will fill.XPositionPercent
- Defines the percentage point horizontally within the viewable space that the content should be centered on.YPositionPercent
- Defines the percentage point vertically within the viewable space that the content should be centered on.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 DataTemplate
s for UpbeatUI.
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(),
});
}