Currently, the only way to show any type of dialog with Prism is by using he PopupWindowAction in combination with System.Windows.Interactivity. To be honest, I really dislike this approach. It's over complex, highly verbose, difficult to implement, and is very limited. The limitations are covered pretty well in Issue #864
Instead, I would like to create a new IDialogService API that will replace the PopupWindowAction altogether. This service will allow developers to show any dialog they want either modal, or non-modal, and have complete control over their dialog logic.
The current implementation looks like this:
public interface IDialogService
{
void Show(string name, IDialogParameters parameters, Action<IDialogResult> callback);
void ShowDialog(string name, IDialogParameters parameters, Action<IDialogResult> callback);
}
The idea here is that Prism will no longer provide any built-in dialogs like Notification or Confirmation. Mainly because the Prism implementations are UGLY and will never match the styling of your beautiful WPF application. So, it's important that you are able to register your own dialogs.
Your dialog view is a simple UserControl that can be designed anyway you please. The only requirement it has a ViewModel that implements IDialogAware set as it's DataContext. Preferrably, it will utilize the ViewModelLocator
<UserControl x:Class="HelloWorld.Dialogs.NotificationDialog"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:prism="http://prismlibrary.com/"
prism:ViewModelLocator.AutoWireViewModel="True"
Width="300" Height="150">
<Grid x:Name="LayoutRoot" Margin="5">
<Grid.RowDefinitions>
<RowDefinition />
<RowDefinition Height="Auto" />
</Grid.RowDefinitions>
<TextBlock Text="{Binding Message}" HorizontalAlignment="Stretch" VerticalAlignment="Stretch" Grid.Row="0" TextWrapping="Wrap" />
<Button Command="{Binding CloseDialogCommand}" Content="OK" Width="75" Height="25" HorizontalAlignment="Right" Margin="0,10,0,0" Grid.Row="1" IsDefault="True" />
</Grid>
</UserControl>
Next you need a ViewModel that implements IDialogAware which is defined as follows
public interface IDialogAware
{
bool CanCloseDialog();
string IconSource { get; set; }
void OnDialogClosed();
void OnDialogOpened(IDialogParameters parameters);
string Title { get; set; }
event Action<IDialogResult> RequestClose;
}
There is currently a DialogViewModelBase class that implements this for you to make it easier to create your dialog VMs.
public class DialogViewModelBase : BindableBase, IDialogAware
{
private string _iconSource;
public string IconSource
{
get { return _iconSource; }
set { SetProperty(ref _iconSource, value); }
}
private string _title;
public string Title
{
get { return _title; }
set { SetProperty(ref _title, value); }
}
public event Action<IDialogResult> RequestClose;
public virtual void RaiseRequestClose(IDialogResult dialogResult)
{
RequestClose?.Invoke(dialogResult);
}
public virtual bool CanCloseDialog()
{
return true;
}
public virtual void OnDialogClosed()
{
}
public virtual void OnDialogOpened(IDialogParameters parameters)
{
}
}
In your VM, you place any new properties and methods that you need in order for your dialog to function. The important thing is when you want to close the dialog, just call RaiseCloseDialog and provide your DialogResult.
public class NotificationDialogViewModel : DialogViewModelBase
{
private string _message;
public string Message
{
get { return _message; }
set { SetProperty(ref _message, value); }
}
public DelegateCommand CloseDialogCommand { get; set; }
public NotificationDialogViewModel()
{
Title = "Notification";
CloseDialogCommand = new DelegateCommand(CloseDialog);
}
private void CloseDialog()
{
RaiseRequestClose(new DialogResult(true));
}
public override void OnDialogOpened(IDialogParameters parameters)
{
Message = parameters.GetValue<string>("message");
}
}
To register a dialog, you must have a View (UserControl) and a corresponding ViewModel (which must implement IDialogAware). In the RegisterTypes method, simply register your dialog like you would any other service by using the IContainterRegistery.RegisterDialog method.
protected override void RegisterTypes(IContainerRegistry containerRegistry)
{
containerRegistry.RegisterDialog<NotificationDialog, NotificationDialogViewModel>();
}
To use the dialog service you simply ask for the service in your VM ctor.
public MainWindowViewModel(IDialogService dialogService)
{
_dialogService = dialogService;
}
Then call either Show or ShowDialog providing the name of the dialog, any parameters your dialogs requires, and then handle the result via a call back
private void ShowDialog()
{
var message = "This is a message that should be shown in the dialog.";
//using the dialog service as-is
_dialogService.ShowDialog("NotificationDialog", new DialogParameters($"message={message}"), r =>
{
if (!r.Result.HasValue)
Title = "Result is null";
else if (r.Result == true)
Title = "Result is True";
else if (r.Result == false)
Title = "Result is False";
else
Title = "What the hell did you do?";
});
}
The intent of the dialog API is not to try and guess exactly what type of parameters your need for all of your dialogs, but rather to just create and show the dialogs. To simplify common dialogs in your application the guidance will be to create an extension methods to simplify your applications dialogs.
For example:
public static class DialogServiceEstensions
{
public static void ShowNotification(this IDialogService dialogService, string message, Action<IDialogResult> callBack)
{
dialogService.ShowDialog("NotificationDialog", new DialogParameters($"message={message}"), callBack);
}
}
Then to call your Notifications use the new and improved API that you created specifically for your app.
_dialogService.ShowNotification(message, r =>
{
if (!r.Result.HasValue)
Title = "Result is null";
else if (r.Result == true)
Title = "Result is True";
else if (r.Result == false)
Title = "Result is False";
else
Title = "What the hell did you do?";
});
You can check out the current dialog code here: https://github.com/PrismLibrary/Prism/tree/Interactivity-Improvements/Source/Wpf/Prism.Wpf/Services/Dialogs
You can also play with the sample here: https://github.com/PrismLibrary/Prism/tree/Interactivity-Improvements/Sandbox/Wpf/HelloWorld
Play with it, tell me what you think. Experiment, and try to find what does or doesn't work for you.
A couple of things that I know I still need to figure out
bool? Result and IDialogParameters ParametersTo clarify, this is to replace the PopupWindowAction. I want to remove that mess completely from Prism
What does everyone think?
I like this design very much and for the question:
How to create an extension point to provide custom Windows to act as the dialog host (think Infragistics, DevExpress, Telerik, etc)?
Since we'll have IContainterRegistery.RegisterDialog then why not also have IContainterRegistery.RegisterDialogHost that we allow registering a Dialog Host Window that implements IDialogWindow.
That way the developers can fully design and customize the Dialog Host Window as they want and not relay on Prism set the custom WindowsSettings.
@keyse That's a great idea. I will play around with that.
I have just submitted PR #1682
Please add to guidelines for creating a custom window:
Title binding (e.g. it's not much but I hope it saves other devs a few minutes. :)
@gojanpaolo Thanks for the tip
Hi @brianlagunas
Wouldn't it be nice if ShowDialog returns IDialogResult itself instead of void and remove callback parameter altogether?
If a third party Window closes naturally is there any way to fire the CloseDialogCommand ?
@noufionline While technically possible, I didn't take that approach because this would result in two different API experiences. I was trying to keep the API consistent no matter which method you used.
Both the links you provided are not working. It’s giving a 404
What is the benefit of using callback parameter instead of returning IDialogResult (assuming we can change both methods for consistency)?
You can't return an IDialogResult for the Show method as that is not a thread blocking operation and is not an async method that can be awaited. The call back gives you the opportunity to respond to the dialog being closed when it occurs. Without the call back, you would not know when the diaog has closed or be able to gain access to the IDialogResult.
Ahh! forgot about the modal/non-modal thing. Thanks for the explanation. :)
Does that support multi-window? If main window is on screen 1, and the view whose view model wants to show a dialog is in a window on screen 2, will the dialog properly show up in screen 2?
AFAIK Interaction requests solve that problem, and it would not be too difficult to make them easier to use (I've done it in my project)
This is cool! I have some feedback:
<prism:InteractionRequestTrigger SourceObject="{Binding EditNotificationRequest, Mode=OneWay}">
<prism:PopupWindowAction IsModal="True" CenterOverAssociatedObject="True">
<prism:PopupWindowAction.WindowStyle>
<Style TargetType="Window">
<Setter Property="ResizeMode" Value="NoResize"/>
<Setter Property="ShowInTaskbar" Value="False"/>
<Setter Property="SizeToContent" Value="WidthAndHeight"/>
</Style>
</prism:PopupWindowAction.WindowStyle>
<prism:PopupWindowAction.WindowContent>
<views:EditControl/>
</prism:PopupWindowAction.WindowContent>
</prism:PopupWindowAction>
</prism:InteractionRequestTrigger>
If I'm using ViewModelLocator.AutoWireViewModel, why do I also have to explicitly call containerRegistry.RegisterDialog too? And it seems like both are required.
The Title property on IDialogAware… any chance you could make it something more specific like "DialogTitle" to avoid collisions? I already have a property named "Title" on several of my viewmodels (example: a book viewmodel has a title, which refers to the book's title. Not the dialog's title.)
Solved 1. by using RegisterDialogWindow with a custom window, but ideally we'd be able to define multiple custom windows (some resizable, for instance), and then specify which by name, using an overload on ShowDialog. Just want to make sure there isn't loss of functionality vs the old method. Also, custom dialog windows would be a bit simpler if we could just subclass DialogWindow, but we can't because it was defined in XAML. Can you just define this class in code?
Worked around 3. by having my custom RegisterDialogWindow bind to a different property, then "new"ing my existing Title property. Ugly though.
I just submitted PR #1721. You can now use a style to control the properties of the dialog.
You must register the dialog with the container so calling RegisterDialog is required. Technically this is the EXACT same thing as RegisterForNavigation, but I wanted the API to guide devs in the right direction. I was thinking of removing the RegisterDialog methods and just sticking to RegisterForNavigation, but I didn't want to confuse anyone. Using the VML is not required. You just have to set the DataContext of the UserControl.
No, I am not going to change the name of the Title property on the IDialogAware interface. Just implement the interface explicitly.
Thanks! Regarding the icon, according to https://docs.microsoft.com/en-us/windows/desktop/uxguide/win-dialog-box dialog boxes shouldn't have one.
Then don't provide one. This service can be used to show more than just dialogs.
It’s called dialogservice! I just think if you’re making only a handful of useful window properties available directly, the icon is an odd choice. #1721 would let people add an icon if that’s their perogative. Just offering feedback on your API. I certainly won’t lose sleep if you leave it as-is.
Actually, the only valid argument for removing the Icon property is because by default WPF will assign an icon to a Window. It will use either the ApplicationIcon or the default icon. By forcing the use of the Icon property and not setting a value, it will override the ApplicationIcon (if set) to the default icon.
Hello Brian,
Great job!
What about Open File/Folder dialogs from Microsoft.Win32 or WinForms?
Lukas
@hronlukas You can always create a wrapper/service for those dialogs.
c#
public class OpenFileDialogService : IOpenFileDialogService
{
private readonly OpenFileDialog _openFileDialog = new OpenFileDialog();
public bool? ShowDialog() => _openFileDialog.ShowDialog();
}
Sorry to reiterate the issue, but am I the only one concerned by how to specify the window owning the dialog to avoid that it shows up on a different screen?
Are there recommendations for that? Or this solution only for single-window apps?
@antoinebj Have you tried it yet? Please try it, then provide feedback.
@brianlagunas Indeed, it gets the active window and uses it as owner of the dialog box, so it gets the job done for the vast majority of cases, where you would have a classic dialog box, with its own window. Smart and simple.
In the broader sense, a dialog box could be a popup that appears for user input over a specific content, using the adorner layer for example. In such more or less edge cases, there are alternatives which will be just fine to complement this dialog service.
Good job and thanks for keeping this project alive.
EDIT: nevermind, simply bind the Title property to Title. See https://github.com/PrismLibrary/Prism/issues/1666#issuecomment-463038782
Maybe I miss something regarding the DialogWindowHost: if I don't set the Title property of it, could the UserControl Title override this property somehow? Otherwise there won't be any Title shown.
Despite that, great work!
I would like to have DialogService with methods like ShowMessageBox or other type of doalogs, which are not fits to scenario with custom dialog window and/or views (and viewmodels). My question is what is the best way to extend implementation of class DialogService : IDialogService provided by the library? And if I write my own implementation, should I register it somehow or just inherit DialogService (if I register it I will have two regietered classes) ?
Hello!
Sometimes in a dialog UI you want to show more than a question and buttons, for instance a list of items and you want to return the choosen item. I believe to accomplish this the IDialogAware and IDialogResult could be IDialogAware<T> and IDialogResult<T> and it could a have property <T> Data, this way the Dialog ViewModel is able to return more data, if required.
public class NotificationDialogViewModel : BindableBase, IDialogAware<MyCustomType>
{
public event Action<IDialogResult<MyCustomType>> RequestClose;
protected virtual void CloseDialog(string parameter)
{
bool? result = null;
if (parameter?.ToLower() == "true")
result = true;
else if (parameter?.ToLower() == "false")
result = false;
var dialogResult = new DialogResult<MyCustomType>(result);
dialogResult.Data = myCustomTypeInstance;
RaiseRequestClose(dialogResult);
}
}
@brianlagunas What do you think about this? May I try to implement this and send a PR?
@quicoli you already have the Parameters collection to add as much data as you want.
@brianlagunas Indeed, it gets the active window and uses it as owner of the dialog box, so it gets the job done for the vast majority of cases, where you would have a classic dialog box, with its own window. Smart and simple.
In the broader sense, a dialog box could be a popup that appears for user input over a specific content, using the adorner layer for example. In such more or less edge cases, there are alternatives which will be just fine to complement this dialog service.
Good job and thanks for keeping this project alive.
Hello, can you check DialogWindow's Owner whether is null during configuration?
DialogService.cs
//TODO: is there a better way to set the owner
window.Owner = Application.Current.Windows.OfType
(I have custom dialog window and I can set my Owner in constructor...)
So basically, if the owner is already set, then don't override it? Yeah, that makes sense. Can you submit a PR?
So basically, if the owner is already set, then don't override it? Yeah, that makes sense. Can you submit a PR?
I created a PR #1815.
DialogViewModelBase was removed.
As of now a custom implementation of IDialogAware is expected on the dialog view model.
Reference Issue: https://github.com/PrismLibrary/Prism/issues/1747
@brianlagunas
I can't believe you scraped InteractionRequests. One of the reasons I started using Prism was interaction requests, because they were every elegant solution for reacting to viewmodel requests (not only to showing popups). IDialogService is not replacement of InteractionRequests, because InteractionRequests has higher abstraction level than IDialogService.
Using a IDialogService now break the abstraction between business logic and the presentation, because now the viewmodel knows that it should open a popup (which is view specific) and deal with IDialogResult.
What if I decide that I want to move away from using popups and use different approach in my presentation, like showing popup contents in side panels that have their visibility toggled? Why I should modify my viewmodels when I change the presentation?
Was it necessary to make IInteractionTrigger absolete? Can both approaches to this problem co-exists in Prism?
@ekalchev InteractionRequests are the least elegant solution to showing a dialog that ever existed. Not only is it over complicated, but there are a ton of issues with the current implementation. No abstraction is broken between the view and VM. Opening a dialog via an interface vs. a convoluted event system is no different. Your VM has knowledge of the action that is being taken, however it doesn't know the details of what is being done in response to that action. Which by the way, is the responsibility of a VM.
If you're going to change the way you represent your UI, there will always be code changes required. The "what if" arguments are never a way to try a prove a point because they hardly ever happen. Either way, you usually always update your ViewModel when changes are made to the view. Your VM is your view state. They go hand-in-hand.
Like I said, the InteractionRequest has many problems and is obsolete. If you need this code in your app, take it and use it. Prism will not be supporting it any longer.
Great work this is now natively implemented! I also never used that Interaction Popup Request approach because I found it too complicated to implement for the job to be done.
About two years ago I implemented a IDialogService in my app nearly the same way as you did.
But without that IDialogAware requirement.
Why is there a need to implement IDialogAware interface? AFAIK So far the xxxAware interfaces were optional and could be implemented IF you needed that extra functionality.
Technically it shouldn't really be needed. Making it optional keeps the ViewModel decoupled from any dialog logic, so ANY view can be shown in a dialog. Let's say you want to pop-out a view instance into a window/dialog, or you change your mind and want to display that view on its own dialog. You then simply need to get the IDialogService interface and show it up, without making the view aware of it, because I think it should not make any difference whether the view sits on a dialog or within a region of another window/dialog.
If you don't implement it, you will not be able to initialize your dialog with any data via the parameters, and you will not be able to close it from within your VM. What would be the point in trying to show it in a dialog if you can't do any of those things?
Hi Brian,
would you mind providing correct links to your sample dialog code? I am new to that topic and don't understand where I can register the dialog. Thanks for your help so far!
Check the release notes: https://github.com/PrismLibrary/Prism/releases/tag/v7.2.0.1367
Thanks. Still don't get the point :( in my App.xml.cs there is no method "RegisterTypes(...)" to overwrite.
So far we used DryContainer.Register<>()
Are you using the bootstrapper? If you're using PrismApplication you MUST have a register types method or it won't even build..
Using DryIoc throws an ContainerException while creating the dialog(Implementation type is not specified when using automatic constructor selection). Any ideas regarding this issue?
@lebtho1878 if you have a question please ask it on Stack Overflow, if you believe you have found a bug you should be opening a new issue with a sample that reproduces the issue not asking questions on an old and resolved issue.
Most helpful comment
I like this design very much and for the question:
Since we'll have IContainterRegistery.RegisterDialog then why not also have IContainterRegistery.RegisterDialogHost that we allow registering a Dialog Host Window that implements IDialogWindow.
That way the developers can fully design and customize the Dialog Host Window as they want and not relay on Prism set the custom WindowsSettings.