Reputation: 693
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
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
DialogViewModel
Dialog
, which will extend Window
. It will use a DataTemplate
to show its content based on the DialogViewModel
(or subtypes) in the Dialog.DataContext
DialogViewModel
in your view model that needs to show the dialog.MainWindow
actually show the Dialog
.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));
}
}
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>
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));
}
}
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>
You can improve reusability by moving the code implemented in MainWindow
to an attached behavior.
Upvotes: 2