A Dialog Service for WinUI 3 | XAML Brewer, by Diederik Krols
source link: https://xamlbrewer.wordpress.com/2022/03/09/a-dialog-service-for-winui-3/
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
A Dialog Service for WinUI 3
I this article we build an MVVM DialogService in a WinUI 3 Desktop application. It comes with the following features:
- Message Dialog, Confirmation Dialog, Input Dialog
- Works with {Binding} and {x:Bind}
- Bindable to Command and Event Handler
- Callable from View, ViewModel, Service and more
- Theme-aware
Here’s a screenshot from the corresponding demo app:
The core class of the dialog service is based on the ModalView static class that we built long ago in a similar solution for UWP. It programmatically creates ContentDialog instances for which you can provide the title, and the content of all buttons. Here’s an extract of the original code:
var
dialog =
new
ContentDialog
{
Title = title,
PrimaryButtonText = yesButtonText,
SecondaryButtonText = noButtonText,
CloseButtonText = cancelButtonText
};
When you run this code in a WinUI 3 Desktop app, nothing really happens. You get a “This element is already associated with a XamlRoot, it cannot be associated with a different one until it is removed from the previous XamlRoot.” exception:
The screenshot above comes from a click event handler. The app crashes and tell you what’s wrong, and that’s OK. However, when using a Command to open the dialog, the binding engine swallows the exception, and the app continues to run without the dialog opening. It’s not a bug -it’s what the binding engine does- but it smells like a bug, so people logged issues in WinUI and people logged issues in Prism.
When programmatically instantiating a ContentDialog in WinUI 3 -even in a View or Page- you need to provide a XamlRoot. We decided to transform our static methods into extension methods for FrameworkElement. Not only does this class come with a XamlRoot property, it also has a RequestedTheme that we can pass to ensure that the dialog follows the app’s theme. Here’s the set of methods to open a message box – a content dialog with a title, a piece of text (the message), and one single button (the classic OK button). It returns no result.
public
static
async
Task MessageDialogAsync(
this
FrameworkElement element,
string
title,
string
message)
{
await
MessageDialogAsync(element, title, message,
"OK"
);
}
public
static
async
Task MessageDialogAsync(
this
FrameworkElement element,
string
title,
string
message,
string
buttonText)
{
var
dialog =
new
ContentDialog
{
Title = title,
Content = message,
CloseButtonText = buttonText,
XamlRoot = element.XamlRoot,
RequestedTheme = element.ActualTheme
};
await
dialog.ShowAsync();
}
For each of the dialog types, the call to the extension method in the ModalView class is more or less the same. Only the return type is different: void, bool, nullable bool, string, …
For the first test, we create a button in the View, and call a classic event handler:
<
Button
Content
=
"Message Dialog"
Click
=
"MessageBox_Click"
/>
In the event handler, we use the page itself (this) as the Framework element to pass. Here’s the call:
private
async
void
MessageBox_Click(
object
sender, RoutedEventArgs e)
{
await
this
.MessageDialogAsync(
"All we are saying:"
,
"Give peace a chance."
,
"Got it"
);
}
Here’s how the result looks like:
Our second dialog type is the ConfirmationDialog, with a title and two (Yes/No) or three (Yes/No/Cancel) buttons. Here’s the main extension method for this one:
public
static
async
Task<
bool
?> ConfirmationDialogAsync(
this
FrameworkElement element,
string
title,
string
yesButtonText,
string
noButtonText,
string
cancelButtonText)
{
var
dialog =
new
ContentDialog
{
Title = title,
PrimaryButtonText = yesButtonText,
SecondaryButtonText = noButtonText,
CloseButtonText = cancelButtonText,
XamlRoot = element.XamlRoot,
RequestedTheme = element.ActualTheme
};
var
result =
await
dialog.ShowAsync();
if
(result == ContentDialogResult.None)
{
return
null
;
}
return
(result == ContentDialogResult.Primary);
}
To test it, we added a ViewModel, set it as DataContext to the View, and added a button:
<
Button
Content
=
"2-Button Confirmation Dialog"
Command
=
"{Binding ConfirmationCommandYesNo}"
/>
Our next step was finding an appropriate Framework element to pass – not easy in a ViewModel that is unaware of the View it’s bound to. In UWP we could use Window.Current (and its Content). In WinUI 3 Window.Current is still in the API but always returns null. As an alternative we declared a MainRoot property in our application class. It refers to the Content element of the main window of our app, that we traditionally named ‘Shell’. Here’s the declaration and initialization:
public
partial
class
App : Application
{
private
Shell shell;
public
static
FrameworkElement MainRoot {
get
;
private
set
; }
protected
override
void
OnLaunched(Microsoft.UI.Xaml.LaunchActivatedEventArgs args)
{
shell =
new
Shell();
shell.Activate();
MainRoot = shell.Content
as
FrameworkElement;
}
}
When you’re building a multi-window app, you will probably need to change that logic. Anyway, all ViewModels and Services now have access to a Framework element to pass to the dialog service. Here’s how the ViewModel from our sample app opens a 2-Button Confirmation Dialog via an AsyncRelayCommand:
public
ICommand ConfirmationCommandYesNo =>
new
AsyncRelayCommand(ConfirmationYesNo_Executed);
private
async
Task ConfirmationYesNo_Executed()
{
var
confirmed =
await
App.MainRoot.ConfirmationDialogAsync(
"What Pantone color do you prefer?"
,
"Freedom Blue"
,
"Energizing Yellow"
);
}
This is how the dialog looks like:
The overload (extension) method with three buttons, opens the dialog that you saw in the first screenshot. There’s no need to paste the code here, it’s similar to the previous one.
Let’s jump to another dialog type: an Input Dialog to request a string from the user. In this scenario, we programmatically set a TextBox as Content of the Dialog, and return its text when the Dialog closes:
public
static
async
Task<
string
> InputStringDialogAsync(
this
FrameworkElement element,
string
title,
string
defaultText,
string
okButtonText,
string
cancelButtonText)
{
var
inputTextBox =
new
TextBox
{
AcceptsReturn =
false
,
Height = 32,
Text = defaultText,
SelectionStart = defaultText.Length
};
var
dialog =
new
ContentDialog
{
Content = inputTextBox,
Title = title,
IsSecondaryButtonEnabled =
true
,
PrimaryButtonText = okButtonText,
SecondaryButtonText = cancelButtonText,
XamlRoot = element.XamlRoot,
RequestedTheme = element.ActualTheme
};
if
(
await
dialog.ShowAsync() == ContentDialogResult.Primary)
{
return
inputTextBox.Text;
}
else
{
return
string
.Empty;
}
}
This time we use {x:Bind} to a command in the View:
<Button Content=
"String Input Dialog"
Command=
"{x:Bind ViewModel.InputStringCommand}"
/>
Here’s the code in the ViewModel:
public
ICommand InputStringCommand =>
new
AsyncRelayCommand(InputString_Executed);
private
async
Task InputString_Executed()
{
var
inputString =
await
App.MainRoot.InputStringDialogAsync(
"How can we help you?"
,
"I need ammunition, not a ride."
,
"OK"
,
"Forget it"
);
}
And the result:
The last type of input dialog in this article, is a multi-line text input dialog – typically one that you would use to collect comments or remarks:
public
static
async
Task<
string
> InputTextDialogAsync(
this
FrameworkElement element,
string
title,
string
defaultText)
{
var
inputTextBox =
new
TextBox
{
AcceptsReturn =
true
,
Height = 32 * 6,
Text = defaultText,
TextWrapping = TextWrapping.Wrap,
SelectionStart = defaultText.Length
};
var
dialog =
new
ContentDialog
{
Content = inputTextBox,
Title = title,
IsSecondaryButtonEnabled =
true
,
PrimaryButtonText =
"Ok"
,
SecondaryButtonText =
"Cancel"
,
XamlRoot = element.XamlRoot,
RequestedTheme = element.ActualTheme
};
if
(
await
dialog.ShowAsync() == ContentDialogResult.Primary)
{
return
inputTextBox.Text;
}
else
{
return
string
.Empty;
}
}
For the sake of completeness, we’ll bind it to an event handler in the ViewModel:
<
Button
Content
=
"Text Input Dialog"
Click
=
"{x:Bind ViewModel.InputText_Click}"
/>
Here’s the code in the ViewModel:
public
async
void
InputText_Click(
object
sender, RoutedEventArgs e)
{
var
inputText =
await
App.MainRoot.InputTextDialogAsync(
"What would Faramir say?"
,
"“War must be, while we defend our lives against a destroyer who would devour all; but I do not love the bright sword for its sharpness, nor the arrow for its swiftness, nor the warrior for his glory. I love only that which they defend.”\n\nJ.R.R. Tolkien"
);
}
And this is what it looks like at runtime:
With just a handful lines of code, we built a dialog service for WinUI 3 Desktop applications. Our sample solution lives here in GitHub. Feel free to add your own dialogs, like for numeric or date input.
Enjoy!
Recommend
About Joyk
Aggregate valuable and interesting links.
Joyk means Joy of geeK