Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[WPF] New IDialogService #1682

Merged
merged 6 commits into from
Feb 7, 2019
Merged

[WPF] New IDialogService #1682

merged 6 commits into from
Feb 7, 2019

Conversation

brianlagunas
Copy link
Member

@brianlagunas brianlagunas commented Feb 7, 2019

Currently, the only way to show any type of dialog with Prism is by using the 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 created 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.

Create Your Dialog View

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}" CommandParameter="true" Content="OK" Width="75" Height="25" HorizontalAlignment="Right" Margin="0,10,0,0" Grid.Row="1" IsDefault="True" />
    </Grid>
</UserControl>

Create Your Dialog ViewModel

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 DelegateCommand<string> _closeDialogCommand;
        public DelegateCommand<string> CloseDialogCommand =>
            _closeDialogCommand ?? (_closeDialogCommand = new DelegateCommand<string>(CloseDialog));

        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;

        protected virtual void CloseDialog(string parameter)
        {
            bool? result = null;

            if (parameter?.ToLower() == "true")
                result = true;
            else if (parameter?.ToLower() == "false")
                result = false;

            RaiseRequestClose(new DialogResult(result));
        }

        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 NotificationDialogViewModel()
        {
            Title = "Notification";
        }

        public override void OnDialogOpened(IDialogParameters parameters)
        {
            Message = parameters.GetValue<string>("message");
        }
    }

Register the Dialog

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>();
 }

Using the Dialog Service

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?";
    });
}

Simplify your Application Dialog APIs

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?";
    });

Register a Custom Dialog Window

It's very common to be using a third-party control vendor such as Infragistics. In these cases, you may want to replace the standard WPF Window control that hosts the dialogs with a custom Window class such as the Infragistics XamRibbonWindow control.

In this case, just create your custom Window, and implement the IDialogWindow interface:

public partial class MyRibbonWindow: XamRibbonWindow, IDialogWindow
{
    public IDialogResult Result { get; set; }
    ….
}

Then register your dialog window with the IContainerRegistry.

 protected override void RegisterTypes(IContainerRegistry containerRegistry)
 {
     containerRegistry.RegisterDialogWindow<MyRibbonWindow>();
 }

To clarify, this is to replace the PopupWindowAction. I want to remove that mess completely from Prism

@brianlagunas brianlagunas changed the title Interactivity improvements [WPF] New IDialogService Feb 7, 2019
@brianlagunas brianlagunas merged commit 388d709 into master Feb 7, 2019
@brianlagunas brianlagunas deleted the Interactivity-Improvements branch February 7, 2019 21:07
@noufalonline
Copy link

@brianlagunas There is small type in Create Your Dialog View Code.

CommandPrameter="true"

@LGTCO
Copy link

LGTCO commented Apr 15, 2019

00
01
02
04

I met this problem as the pictures show. I don't know how it happened. Can you help me solve it?thank you.

@noufionline
Copy link
Contributor

@LGTCO Why you are passing the DialogWindow name as uri. DialogWindow is just the container in which your view will be injected. You must be passing the view which you want to inject inside the DialogWindow.

@LGTCO
Copy link

LGTCO commented Apr 15, 2019

Thank you!I get it

@luis-fss
Copy link

dialogService.ShowDialog("NotificationDialog", new DialogParameters($"message={message}"), callBack);

Is it possible to pass more than one parameter?
For example "message" and "id" (both strings)?

@luis-fss
Copy link

Nevermind.
I just found the answer.
Thanks.

var parameters = new DialogParameters($"?title={title}&message={message}");

@ekalchev
Copy link

ekalchev commented Sep 27, 2019

Wonder why these two methods

void Show(string name, IDialogParameters parameters, Action<IDialogResult> callback);
void ShowDialog(string name, IDialogParameters parameters, Action<IDialogResult> callback);

don't have async interface like this

Task<IDialogResult> ShowAsync(string name, IDialogParameters parameters);
Task<IDialogResult> ShowDialogAsync(string name, IDialogParameters parameters);

@brianlagunas
Copy link
Member Author

@ekalchev dialogs in WPF are not async

@mxsoftware
Copy link

For completeness
https://mahapps.com/controls/dialogs.html

await this.ShowMessageAsync("This is the title", "Some message");

@brianlagunas
Copy link
Member Author

brianlagunas commented Sep 27, 2019

Those aren't doing what you think they're doing. In fact, that entire API is useless as there is only one UI thread in WPF, so using the dispatcher to provide an "async" API but not an async behavior is pointless. Also, those dialogs are UI blocking (think MessageBox.ShowDialog). In Prism the dialogs can live on, be interacted with, and multiple more opened while the main application is also interacted with.

@mxsoftware
Copy link

I agree with your point of view and I don't use Dialog asynchronously myself.
But I think it depends on what point of view things are observed.
In Prism it probably makes no sense to have them, and in this you are the major judge,
But in Wpf-based libraries there are.

@brianlagunas
Copy link
Member Author

Agree to disagree. I wish async/await was never invented. At least with the old school async API's, people had to actually understand how things worked when multi-threading 😄

@mxsoftware
Copy link

Totally agree. I wouldn't want to be off topic, but:
https://devblogs.microsoft.com/dotnet/migrating-delegate-begininvoke-calls-for-net-core/
I think we share this.

@ekalchev
Copy link

ekalchev commented Sep 27, 2019

@ekalchev dialogs in WPF are not async

So why bother to a have callback if you writing interface for blocking call for ShowDialog method?
Just make it simple

IDialogResult ShowDialog(string name, IDialogParameters parameters)

What about Show method, it is non blocking? Callback style programming is in the past.

In fact, that entire API is useless as there is only one UI thread in WPF, so using the dispatcher to provide an "async" API but not an async behavior is pointless.

async doens't mean multithreaded programming. Async programming is perfectly legal with only one thread and javascript do that for decades. https://blogs.msdn.microsoft.com/benwilli/2015/09/10/tasks-are-still-not-threads-and-async-is-not-parallel/

I can give you more examples like this https://github.com/lbugnion/mvvmlight/blob/master/GalaSoft.MvvmLight/GalaSoft.MvvmLight.Platform%20(UWP)/Views/DialogService.cs

@brianlagunas
Copy link
Member Author

@ekalchev There are many reason why the API's are the way they are. A big one is making it easy to extend (replace windows with other window types) with little to no code requirements. Another reason is I wanted to method signature to be the same. A Show is non-block and may never be close, but when it does close you still want to be notified. A ShowDialog is blocking and could have returned the IDialogResult directly, but I didn't want to introduce custom methods on the Window Class, and I wanted the APIs for both methods to be the same so the developer experience is consistent.

You are more than welcome to create your own async dialog interfaces in your apps, but I will not be adding them to Prism as there is no real benefit beside syntactical sugar.

@Pretasoc
Copy link

Pretasoc commented Sep 27, 2019

If you want an async api, you can use an extension method, to wrap the IDialogService

public static void Task<IDialogResult> ShowDialog( this IDialogService dialogService, string name)
{
    var tcs = new TaskCompletionSource<IDialogResult>();
    dialogService.ShowDialog(name, result => tcs.SetResult(result));
    return tcs.Task;
}

@brianlagunas
Copy link
Member Author

Like I said, syntactical sugar 😄

@QuentinSuckling
Copy link

Is there any way for the DialogService to show a dialog that is modal for a single view in WPF?

I've got a tabbed application where it can prompt the user in one view (locking that whole view/tab) but they can tab to another view to check other data etc. then return and respond to the dialog.

Currently I use InteractionRequestTriggers that add the dialog templates as children to the AssociatedObject (as per the original guidance when I started using it 6 or 7 years ago).
The dialog templates are all set using a style in my views so there's no duplication or boilerplating.

I know you plan on removing PopupWindowAction (which I don't use) but I've noticed that all the InteractionRequest classes are marked as obsolete as well.
It would completely break my app if they were removed.

So if there is a way in DialogService to make the dialogs modal locally within the view then I'll happily change everything to use it.
If not, then can you please leave the InteractionRequest classes in Prism when you remove PopupWindowAction?

Thanks,
Quentin

@brianlagunas
Copy link
Member Author

@QuentinSuckling

I've got a tabbed application where it can prompt the user in one view (locking that whole view/tab) but they can tab to another view to check other data etc. then return and respond to the dialog.

That's not how the PopupWindowAction works. The PopupWindowAction shows an actual Window (Window.ShowDialog) which is not capable of blocking a single tab in a tabbed page. For that scenario you must use a different approach which mimics a Window (but is not actually a Window) and can scope it's behavior to the current Tab.

The only thing the PopupWIndowAction does is show a Window. The same thing the new DialogService does. PopupWindowAction will be going away. If you feel you need it, grab the code and bring it into your app.

@QuentinSuckling
Copy link

Hi Brian,

Yep, I understand PopupWindowAction shows an actual window (which is why I don't use it).
I was just asking if the InteractionRequest classes (which I do use) would be staying or not?

Thanks

@DavidWeider
Copy link

Hi!
I have the same problem as "LGTCO, commented on 15 Apr 2019".
I will create a custom dialog with any content, an a dialog result.
I get the same error and i don't understood the problem.
Is there any working example with a custom dialog?

Thanks & best regards!

@brianlagunas
Copy link
Member Author

@lock
Copy link

lock bot commented Feb 8, 2020

This thread has been automatically locked since there has not been any recent activity after it was closed. Please open a new issue for related bugs.

@lock lock bot locked as resolved and limited conversation to collaborators Feb 8, 2020
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
Projects
None yet
Development

Successfully merging this pull request may close these issues.

10 participants