Lucy82
Lucy82

Reputation: 693

Window fade out in MVVM

I have a ViewModel, from which I show a Window that should fade out. It fades only first time, and then It stops.

public class MessageBoxViewModel
{
     private MessageBoxView _message;
     private MessageBoxResult _result = MessageBoxResult.No;

     public MessageBoxViewModel()
     {
        //...creating commands...
     }

     private void Window_Closing(object sender, CancelEventArgs e)
     {
          //Close window with fade out animation
          _message.Closing -= Window_Closing;
          e.Cancel = true;
          var animation = new DoubleAnimation
          {
              To=0,
              Duration=TimeSpan.FromSeconds(1),
              FillBehavior = FillBehavior.Stop
           };
 
           animation.Completed += (s, a) => { _message.Close(); };
           _message.BeginAnimation(UIElement.OpacityProperty, animation);
       }
      
       public MessageBoxResult Show(string Text, string Title)
       {
           //...setting properties which View is bound to

           _message = new MessageBoxView
           {
              DataContext = this
           };
           _message.Closing += Window_Closing;
           _message.ShowDialog();
  
          return _result;
       }
 }

And this is how I call messagebox in different ViewModel:

 class SomeViewMNodel : INotifyPropertyChanged
 {
      private MessageBoxViewModel _message = new MessageBoxViewModel(); 
      public SomeViewModel()
      {
         //....
      }

      private void ShowMessages(object parameter) //Command on click of some button
      {
           _message.Show("Hey I'm fading.", "Fade out"); //Fading is succesfully done

          _message.Show("Hey I'm fading second time.", "Fade out"); //Fading doesn't work anymore
       }

  }

I have tried to stop an animation as suggested here, but that doesn't seem to work. Neither does Opacity property actually change - a simple check with var j = _message.GetAnimationBaseValue(UIElement.OpacityProperty) ==> allways shows value of 1, in animation.Completed or after inicializing new Window.

I've figured that animation works If I don't use _message variable, but instead declare a new instance of ViewModel, e.g. var win = new MessageBoxViewModel(). But I'm using this custom MessageBox for all errors & notifications in many ViewModels, so I would like to use only _message variable If possible (I would make it global).

Without MVVM and re-initiliazing instance of Window I can make animation working everytime, but how can I animate Window in MVVM properly?

Upvotes: 1

Views: 477

Answers (1)

BionicCode
BionicCode

Reputation: 28968

I don't see the point in making the Window instance global and reuse the Window instance. You should generally avoid creating global (static) instances.
Creating a small Window instance from time to time is quite affordable. Instantiation costs are unnoticeable in this case.

Anyway, if you want to reuse a Window instance, you are not allowed to close it. Closing a Window disposes its unmanaged resources.

If you want to use Window.Close(), you have to override the virtual Window.OnClosing method (or listen to the Window.Closing event) and cancel closing and instead set the window's visibility to Visibilty.Collapsed:

private void Window_Closing(object sender, CancelEventArgs e)
{
  //Close window with fade out animation
  _message.Closing -= Window_Closing;
  e.Cancel = true;
  var animation = new DoubleAnimation
  {
    To=0,
    Duration=TimeSpan.FromSeconds(1),
    FillBehavior = FillBehavior.Stop
  };

  animation.Completed += (s, a) => _message.Visibility = Visibility.Collapsed; ;

  _message.BeginAnimation(UIElement.OpacityProperty, animation);
}

But as some have noticed already, this implementation is violating the MVVM pattern. You are not allowed to introduce a coupling between the view and the view model. The goal of MVVM is to remove this exact coupling.
The view model is not allowed to handle any UI components.

The following example shows how implement a dialog infrastructure that complies with the MVVM pattern.

The example consists of four simple steps

  1. Create a DialogViewModel
  2. Create the Dialog, which will extend Window. It will use a DataTemplate to show its content based on the DialogViewModel (or subtypes) in the Dialog.DataContext
  3. Create a public property e.g., DialogViewModel in your view model that needs to show the dialog.
  4. Let the parent control e.g. MainWindow actually show the Dialog.

Example implementation

  1. Implement a view model that serves as the data source for the actual dialog window:

    DialogViewModel.cs
    This view model defines an AcceptCommand and a CancelCommand which can be bound to corresponding dialog buttons.
    When one of the commands is executed a CloseRequested event is raised.
    The constructor takes a delegate which serves as a callback that is invoked when the dialog was closed.

     public class DialogViewModel : INotifyPropertyChanged
     {
       public DialogViewModel(string dialogCaption, string message, Action<DialogViewModel> dialogClosedHandler)
       {
         this.DialogCaption = dialogCaption;
         this.Message = message;
         this.DialogClosedHandler = dialogClosedHandler;
       }
    
       public void HandleResult() => this.DialogClosedHandler.Invoke(this);
    
       private string dialogCaption;
       public string DialogCaption
       {
         get => this.dialogCaption;
         set
         {
           this.dialogCaption = value;
           OnPropertyChanged();
         }
       }
    
       private string message;
       public string Message
       {
         get => this.message;
         set
         {
           this.message = value;
           OnPropertyChanged();
         }
       }
    
       public ICommand AcceptCommand => new RelayCommand(param => this.IsAccepted = true);
       public ICommand CancelCommand => new RelayCommand(param => this.IsAccepted = false);
    
       private bool isAccepted;
       public bool IsAccepted
       {
         get => this.isAccepted;
         set
         {
           this.isAccepted = value;
           OnPropertyChanged();
           OnCloseRequested();
         }
       }
    
       public event EventHandler<DialogEventArgs> CloseRequested;
       public event PropertyChangedEventHandler PropertyChanged;
    
       private Action<DialogViewModel> DialogClosedHandler { get; }
    
       protected virtual void OnCloseRequested()
       {
         this.CloseRequested?.Invoke(this, new DialogEventArgs(this));
       }
    
       protected virtual void OnPropertyChanged([CallerMemberName] string propertyName = null)
       {
         this.PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
       }
     }
    
  2. Implement the dialog window, which will show its content based on the DialogViewModel using a DataTemplate. To create different types of specialized dialogs, simply ceate a specialized dialog view model and a corresponding DataTemplate.

    Every eventual dialog animation is also implemented in this class using XAML and EventTrigger, which triggers on a DialogClosed routed event.

    The Dialog will listen to the DialogViewModel.CloseRequested event to close itself. Since you wished to reuse the Dialog instance, the Dialog intercepts the invocation of Close() to collapse itself. This behavior can be enabled using the constructor.

    After closing itself, the Dialog sets the DialogEventArgs.Handled property to true, which will trigger the invocation of the dialog closed callback (which was registered with the DialogViewModel), so that the calling view model, that showed the dialog, can continue to execute:

    Dialog.xaml.cs

     public partial class Dialog : Window
     {
       #region DialogClosedRoutedEvent
    
       public static readonly RoutedEvent DialogClosedRoutedEvent = EventManager.RegisterRoutedEvent(
         "DialogClosed",
         RoutingStrategy.Bubble,
         typeof(RoutedEventHandler),
         typeof(Dialog));
    
       public event RoutedEventHandler DialogClosed
       {
         add => AddHandler(Dialog.DialogClosedRoutedEvent, value);
         remove => RemoveHandler(Dialog.DialogClosedRoutedEvent, value);
       }
    
       #endregion
    
       private bool IsReuseEnabled { get; }
    
       public Dialog(bool isReuseEnabled = false)
       {
         InitializeComponent();
         this.IsReuseEnabled = isReuseEnabled;
         this.DataContextChanged += OnDialogViewModelChanged;
       }
    
       public Dialog(DialogViewModel dialogViewModel) : this()
       {
         this.DataContext = dialogViewModel;
       }
    
       private void OnDialogViewModelChanged(object sender, DependencyPropertyChangedEventArgs e)
       {
         HandleDialogNewViewModel(e.OldValue as DialogViewModel, e.NewValue as DialogViewModel);
       }
    
       private void HandleDialogNewViewModel(DialogViewModel oldDialogViewModel, DialogViewModel newDialogViewModel)
       {
         if (oldDialogViewModel != null)
         {
           oldDialogViewModel.CloseRequested -= CloseDialog;
         }
    
         if (newDialogViewModel != null)
         {
           newDialogViewModel.CloseRequested += CloseDialog;
         }
       }
    
       private void CloseDialog(object sender, DialogEventArgs e)
       {
         Close();
         e.Handled = true;
       }
    
       #region Overrides of Window
    
       protected override void OnClosing(CancelEventArgs e)
       {
         if (!this.IsReuseEnabled)
         {
           return;
         } 
    
         e.Cancel = true;
         Dispatcher.InvokeAsync(
           () => RaiseEvent(new RoutedEventArgs(Dialog.DialogClosedRoutedEvent, this)),
           DispatcherPriority.Background);
         base.OnClosing(e);
       }
    
       #endregion
    
       private void DialogClosedAnimation_OnCompleted(object? sender, EventArgs e)
       {
         this.Visibility = Visibility.Collapsed;
       }
     }
    

    Dialog.xaml
    To customize the layout, edit the DataTemplate:

     <Window x:Class="Dialog"
             xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
             xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
             xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
             xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
             mc:Ignorable="d"
             Height="450"
             Width="800" 
             Title="{Binding DialogCaption}">
       <Window.Resources>
    
         <!-- 
           To create more specialized dialogs, 
           create a dedicated DataTemplate for each dialog view model type.
         -->
    
         <DataTemplate DataType="{x:Type local:DialogViewModel}">
           <StackPanel>
             <TextBlock Text="{Binding Message}"/>
             <StackPanel Orientation="Horizontal">
               <Button Content="Ok" Command="{Binding AcceptCommand}" />
               <Button Content="Cancel" IsDefault="True" IsCancel="True" Command="{Binding CancelCommand}" />
             </StackPanel>
           </StackPanel>
         </DataTemplate>
       </Window.Resources>
    
       <!-- Animations triggered by the DialogClosed event -->
       <Window.Triggers>
         <EventTrigger RoutedEvent="local:Dialog.DialogClosed">
           <BeginStoryboard>
             <Storyboard Completed="DialogClosedAnimation_OnCompleted">
               <DoubleAnimation Storyboard.TargetProperty="Opacity"  To="0" Duration="0:0:1" FillBehavior="Stop"/>
               <ObjectAnimationUsingKeyFrames Storyboard.TargetProperty="Visibility" FillBehavior="Stop">
                 <DiscreteObjectKeyFrame KeyTime="0:0:1" Value="{x:Static Visibility.Hidden}"/>
               </ObjectAnimationUsingKeyFrames>
             </Storyboard>
           </BeginStoryboard>
         </EventTrigger>
       </Window.Triggers>
    
       <Grid>
         <ContentPresenter Content="{Binding}"/>
       </Grid>
     </Window>
    
  3. To trigger the displaying of the Dialog let the view model create the DialogViewModel and assign it to a public property.

    ViewModel.cs

     public class ViewModel : INotifyPropertyChanged
     {
       public void SaveToFile(object data, string filePath)
       {
         // Check if file exists (pseudo)
         if (string.IsNullOrWhiteSpace(filePath))
         {
           // Show the dialog
           this.DialogViewModel = new DialogViewModel("File Exists Dialog", "File exists. Replace file?", OnDialogResultAvailable);
         }
         else
         {
           Save(data, filePath);
         }
       }
    
       public void Save(object data, string filePath)
       {
         // Write data to file
       }
    
       private void OnDialogResultAvailable(DialogViewModel dialogViewModel)
       {
         if (dialogViewModel.IsAccepted)
         { 
           // User has accepted to overwrite file             
           Save(data, filePath);
         }
       }
    
       private DialogViewModel dialogViewModel;
       public DialogViewModel DialogViewModel
       {
         get => this.dialogViewModel;
         set
         {
           this.dialogViewModel = value;
           OnPropertyChanged();
         }
       }
    
       public event PropertyChangedEventHandler PropertyChanged;
       protected virtual void OnPropertyChanged([CallerMemberName] string propertyName = null)
       {
         this.PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
       }
     }
    
  4. To actually show the dialog, let the parent Window (e.g., MainWindow) listen to the property changes of ViewModel.DialogViewModel e.g., by setting up a Binding:

    MainWindow.xaml.cs

    public partial class MainWindow : Window
    {
      public static readonly DependencyProperty CurrentDialogViewModelProperty = DependencyProperty.Register(
        "CurrentDialogViewModel",
        typeof(DialogViewModel),
        typeof(MainWindow),
        new PropertyMetadata(default(DialogViewModel), MainWindow.OnDialogViewModelChanged));
    
      public DialogViewModel CurrentDialogViewModel
      {
        get => (DialogViewModel) GetValue(MainWindow.CurrentDialogViewModelProperty);
        set => SetValue(MainWindow.CurrentDialogViewModelProperty, value);
      }
    
      private static void OnDialogViewModelChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
      {
        if (e.NewValue == null)
        {
          return;
        }
    
        (d as MainWindow).ShowDialog(e.NewValue as DialogViewModel);
      }
    
      private void ShowDialog(DialogViewModel dialogViewModel)
      {
        this.Dialog.DataContext = dialogViewModel;
        this.Dialog.ShowDialog();
    
        // Alternative recommended approach:
        // var dialog = new Dialog(dialogViewModel);
        // dialog.ShowDialog();
      }
    
      private Dialog Dialog { get; set; }
    
      public MainWindow()
      {
        InitializeComponent();
    
        // Create a reusable dial instance (not recommended)
        this.Dialog = new Dialog(true);
      }
    
      private void Button_Click(object sender, RoutedEventArgs e)
      {
        (this.DataContext as ViewModel).SaveToFile(null, string.Empty);
      }
    }
    

    MainWindow.xaml

    <Window x:Class="MainWindow"
            xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
            xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
            xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
            xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
            xmlns:local="clr-namespace:"
            mc:Ignorable="d"
            Title="MainWindow" Height="450" Width="800">
      <Window.DataContext>
        <local:ViewModel />
      </Window.DataContext>
      <Window.Resources>
        <Style TargetType="local:MainWindow">
    
          <-- Observe view model DialogViewModel property using data binding -->
          <Setter Property="CurrentDialogViewModel" Value="{Binding DialogViewModel}" />
        </Style>
      </Window.Resources>
    
      <Button Content="Show Dialog" Click="Button_Click" />
    </Window>
    

Remarks

You can improve reusability by moving the code implemented in MainWindow to an attached behavior.

Upvotes: 2

Related Questions