Question:
How CORRECTLY to implement the opening of the second window from the button on the first window using the MVVM pattern? I did not find a specific example on the web, and in many projects, opening a window violates the principles of this pattern…
Answer:
If you understand the code, then try DialogService . Sorry, there is not much time to answer in more detail.
You can use it like this:
//просто какие-то входные данные
var settingsWindowRequest = new SettingsLocalEvent() {};
//вид диалога, это UserControl
var view = new ChangeSettingsView(settingsWindowRequest, this);
//диалог сервис
var dialogService = new DialogService();
//вызов диалога
dialogService.ShowDialog<NewSettingsLocalEvent>(
view,
//пояснения к SetNewSettings см ниже
newSettings => SetNewSettings(newSettings),
throwIfNullResult: false,
//imageUri - это моя отсебятина от лени, можно удалять из исходников
imageUri: new Uri("pack://application:,,,/Red.ico", UriKind.RelativeOrAbsolute)
);
<UserControl x:Class="Xyz.View.ChangeSettingsView"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:local="clr-namespace:Xyz.View"
xmlns:lctrl="clr-namespace:Xyz.LocalControls"
xmlns:mvvm="Sborka_S_Ishodnikami_I_Sootvetstvuyushij_Namespace"
mc:Ignorable="d"
d:DesignHeight="390" d:DesignWidth="530">
<mvvm:DialogService.ContainerStyle>
<Style TargetType="mvvm:DialogContainer" BasedOn="{StaticResource OkCancelDialogStyle}">
<Setter Property="Title" Value="{Binding Title}" />
<Setter Property="Width" Value="530"/>
<Setter Property="Height" Value="500"/>
<Setter Property="Title" Value="Настройки"/>
</Style>
</mvvm:DialogService.ContainerStyle>
</UserControl>
In the code above, type in the correct Sborka_S_Ishodnikami_I_Sootvetstvuyushij_Namespace
. Here is an example of the OkCancelDialogStyle style:
<Style x:Key="OkCancelDialogStyle" TargetType="mvvm:DialogContainer">
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="mvvm:DialogContainer">
<Border Background="{TemplateBinding Background}" Padding="{TemplateBinding Padding}">
<controls:BusyIndicator IsBusy="{Binding IsBusy}">
<DockPanel>
<Border Background="WhiteSmoke" MinHeight="50" DockPanel.Dock="Bottom"
BorderBrush="LightGray"
BorderThickness="0,1,0,0"
>
<StackPanel
HorizontalAlignment="Right"
VerticalAlignment="Center"
Orientation="Horizontal"
Height="24"
Margin="0,0,10,0"
>
<Button Content="OK" IsDefault="True" Command="{Binding OkCommand}" MinWidth="60" Margin="0,0,4,0" />
<Button Content="Отмена" Command="{Binding CancelCommand}" IsCancel="True" MinWidth="60" />
</StackPanel>
</Border>
<ContentPresenter />
</DockPanel>
</controls:BusyIndicator>
</Border>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
Just in case, this too:
public partial class ChangeSettingsView : UserControl
{
public ChangeSettingsView(SettingsLocalEvent request, object viewModel)
{
InitializeComponent();
DataContext = viewModel;
}
}
Also in the ViewModel, you can implement the IDialogCloseable interface ( IDialogCloseable<TResponse>
), then there will be an event:
public event DialogCloseEvent<TResponse> DialogClose = delegate { };
You can write the following method in the ViewModel:
protected void RaiseDialogClose(object sender, bool? dialogResult, TResponse result)
{
DialogClose(sender, new DialogCloseEventArgs<TResponse>(dialogResult, result));
}
Then, for example, in some command (ICommand) by clicking the OK button, this event can be called:
private void ExecuteOk(object input)
{
RaiseDialogClose(this, true, new NewSettingsLocalEvent()
{
}
);
}
By the way, at the very beginning of the code in this answer, where it says "you can use it like this", there is such a parameter when calling the dialog: newSettings => SetNewSettings(newSettings)
. If I'm not mistaken, then this Func is just called when you click OK in the dialog. Here SetNewSettings is just some method, the point is that it will be called when OK is pressed (again, if I'm not mistaken).
sources
[StyleTypedProperty(Property="ContainerStyleProperty", StyleTargetType=typeof(DialogContainer))]
public class DialogService : IDialogService
{
public static Style GetContainerStyle(DependencyObject obj)
{
return (Style)obj.GetValue(ContainerStyleProperty);
}
public static void SetContainerStyle(DependencyObject obj, Style value)
{
obj.SetValue(ContainerStyleProperty, value);
}
public static readonly DependencyProperty ContainerStyleProperty =
DependencyProperty.RegisterAttached(
"ContainerStyle",
typeof(Style),
typeof(DialogService),
new PropertyMetadata(null)
{
CoerceValueCallback = ContainerStylePropertyCoerce
}
);
private static object ContainerStylePropertyCoerce(DependencyObject d, object baseValue)
{
var style = baseValue as Style;
if (style != null)
{
if (style.TargetType != typeof(DialogContainer))
{
return DependencyProperty.UnsetValue;
}
return style;
}
return null;
}
public void ShowDialog(FrameworkElement view)
{
this.ShowDialog<object>(view, (DialogCallback<object>)null);
}
public void ShowDialog<T>(FrameworkElement view, Action<T> onSuccess, bool throwIfNullResult = false, Uri imageUri = null)
{
this.ShowDialog<T>(
view,
(dr, result) =>
{
if (dr == true && onSuccess != null) onSuccess(result);
},
throwIfNullResult,
imageUri
);
}
public void ShowDialog<T>(FrameworkElement view, DialogCallback<T> callback, bool throwIfNullResult = false, Uri imageUri = null)
{
var container = new DialogContainer(view);
if (imageUri != null)
container.Icon = BitmapFrame.Create(imageUri);
var dialogResult = container.ShowDialog();
if (callback != null)
{
T result = default(T);
if (dialogResult == true)
{
if (throwIfNullResult && container.DialogContainerResult == null)
{
throw new NullReferenceException("DialogContainerResult");
}
if (container.DialogContainerResult != null)
{
if (container.DialogContainerResult is T)
{
result = (T)container.DialogContainerResult;
}
}
}
callback(dialogResult, result);
}
}
public MessageBoxResult ShowMessage(
string message,
string caption = null,
MessageBoxButton button = MessageBoxButton.OK,
MessageBoxImage icon = MessageBoxImage.None,
MessageBoxResult defaultResult = MessageBoxResult.None,
MessageBoxOptions options = MessageBoxOptions.None
)
{
return MessageBox.Show(message, caption, button, icon, defaultResult, options);
}
}
public delegate void DialogCallback<T>(bool? dialogResult, T parameter);
public interface IDialogService
{
void ShowDialog(FrameworkElement view);
void ShowDialog<T>(FrameworkElement view, Action<T> onSuccess, bool throwIfNullResult = false, Uri imageUri = null);
void ShowDialog<T>(FrameworkElement view, DialogCallback<T> callback, bool throwIfNullResult = false, Uri imageUri = null);
MessageBoxResult ShowMessage(
string message,
string caption = null,
MessageBoxButton button = MessageBoxButton.OK,
MessageBoxImage icon = MessageBoxImage.None,
MessageBoxResult defaultResult = MessageBoxResult.None,
MessageBoxOptions options = MessageBoxOptions.None
);
}
public class DialogContainer : Window
{
static DialogContainer()
{
DefaultStyleKeyProperty.OverrideMetadata(
typeof(DialogContainer),
new FrameworkPropertyMetadata(typeof(DialogContainer))
);
}
//private IDialogCloseable _closeable = null;
private static Style _GetBaseStyle;
private static Style GetBaseStyle
{
get
{
return _GetBaseStyle ?? (_GetBaseStyle = Application.Current.FindResource(typeof(DialogContainer)) as Style);
}
}
internal DialogContainer(FrameworkElement view, Window owner = null)
{
Content = view;
this.Owner = owner
?? Application.Current.Windows.OfType<Window>().Where(x => x.IsActive).FirstOrDefault()
?? Application.Current.MainWindow;
WindowStartupLocation = owner == null
? System.Windows.WindowStartupLocation.CenterOwner
: System.Windows.WindowStartupLocation.CenterScreen;
if (view != null)
{
var containerStyle = DialogService.GetContainerStyle(view);
if (containerStyle != null)
{
if (containerStyle.BasedOn == null)
{
containerStyle.BasedOn = GetBaseStyle;
}
Style = containerStyle;
}
CloseableDescriptor = GetFirstIDialogCloseable(view, view.DataContext);
var dataContextBinding = new Binding()
{
Source = view,
Path = new PropertyPath(FrameworkElement.DataContextProperty)
};
SetBinding(FrameworkElement.DataContextProperty, dataContextBinding);
}
}
private DynamicCloseableDescriptor CloseableDescriptor = null;
private DynamicCloseableDescriptor GetFirstIDialogCloseable(params object[] items)
{
var descriptors = from x in items
where x != null
let icloseable = x.GetType()
.GetInterfaces()
.Where(i => i.IsGenericType && i.GetGenericTypeDefinition() == typeof(IDialogCloseable<>))
.FirstOrDefault()
where icloseable != null
select new DynamicCloseableDescriptor(this, x, icloseable);
var target = descriptors.FirstOrDefault();
return target;
}
private sealed class DynamicCloseableDescriptor : IDisposable
{
private static readonly MethodInfo OpenGenericDelegateMethod;
static DynamicCloseableDescriptor()
{
OpenGenericDelegateMethod = typeof(DialogContainer)
.GetMethods(BindingFlags.NonPublic | BindingFlags.Instance)
.Where(x => x.Name == "DynamicCloseDialog")
.FirstOrDefault();
}
readonly object Target;
readonly Type ICloseableType = null;
readonly Delegate DynamicDelegate = null;
readonly EventInfo DialogCloseEvent = null;
readonly Type DialogResultType = null;
public DynamicCloseableDescriptor(DialogContainer container, object target, Type iCloseableType)
{
if (container == null) throw new ArgumentNullException("container");
if (target == null) throw new ArgumentNullException("target");
if (iCloseableType == null) throw new ArgumentNullException("iCloseableType");
Target = target;
ICloseableType = iCloseableType;
DialogResultType = ICloseableType.GetGenericArguments()[0];
DialogCloseEvent = ICloseableType.GetEvent("DialogClose");
DynamicDelegate = Delegate.CreateDelegate(
DialogCloseEvent.EventHandlerType,
container,
OpenGenericDelegateMethod.MakeGenericMethod(DialogResultType)
);
DialogCloseEvent.AddEventHandler(Target, DynamicDelegate);
}
public void ExecuteCancelIfCan()
{
if (IsDisposed) throw new ObjectDisposedException("DynamicCloseableDescriptor");
var cmd = ((ICancelable)Target).CancelCommand;
if (cmd != null)
{
if (cmd.CanExecute(null))
{
cmd.Execute(null);
}
}
else
{
DynamicDelegate.DynamicInvoke(new[] { Target, null });
}
}
public void Dispose()
{
Dispose(true);
GC.SuppressFinalize(this);
}
private bool IsDisposed = false;
private void Dispose(bool isDisposing)
{
if (!IsDisposed && isDisposing)
{
DialogCloseEvent.RemoveEventHandler(Target, DynamicDelegate);
IsDisposed = true;
}
}
}
internal object DialogContainerResult = null;
/// <summary>
/// Вызывается когда IDialogCloseable запускает DialogCloseEvent.
/// Вызов этого метода строится динамически в DynamicCloseableDescriptor
/// </summary>
/// <typeparam name="T"></typeparam>
/// <param name="sender">Event Sender</param>
/// <param name="e">Event args</param>
private void DynamicCloseDialog<T>(object sender, DialogCloseEventArgs<T> e)
{
if (CloseableDescriptor != null)
{
CloseableDescriptor.Dispose();
CloseableDescriptor = null;
}
if (e != null)
{
if (e.DialogResult == true)
{
DialogContainerResult = e.Result;
}
this.DialogResult = e.DialogResult;
}
else
{
this.DialogResult = false;
}
}
protected override void OnClosing(System.ComponentModel.CancelEventArgs e)
{
if (CloseableDescriptor == null)
{
base.OnClosing(e);
}
else
{
e.Cancel = true;
this.Dispatcher.BeginInvoke((Action)CloseableDescriptor.ExecuteCancelIfCan);
}
}
}
public static class DialogHelper
{
public static void SendOk<T>(this DialogCloseEvent<T> handler, object sender, T result)
{
if (handler != null)
{
handler(sender, DialogCloseEventArgs<T>.Success(result));
}
}
public static void SendCancel<T>(this DialogCloseEvent<T> handler, object sender)
{
if (handler != null)
{
handler(sender, DialogCloseEventArgs<T>.Cancel());
}
}
}
public class DialogCloseEventArgs<T> : EventArgs
{
public bool? DialogResult { get; private set; }
public T Result { get; private set; }
public DialogCloseEventArgs(bool? dialogResult, T result)
{
DialogResult = dialogResult;
Result = result;
}
private static readonly DialogCloseEventArgs<T> CancelResult;
static DialogCloseEventArgs()
{
CancelResult = new DialogCloseEventArgs<T>(false, default(T));
}
public static DialogCloseEventArgs<T> Success(T result)
{
return new DialogCloseEventArgs<T>(true, result);
}
public static DialogCloseEventArgs<T> Cancel()
{
return CancelResult;
}
}
//public delegate void DialogCloseEvent<in T>(bool dialogResult, T result);
/// <summary>
/// Запрос на закрытие диалога
/// </summary>
/// <typeparam name="T">Тип возвращаемого результата</typeparam>
/// <param name="sender">Отправитель</param>
/// <param name="e">Результат диалога, по умолчанию - null = Cancel</param>
public delegate void DialogCloseEvent<T>(object sender, DialogCloseEventArgs<T> e = null);
public interface ICancelable
{
ICommand CancelCommand { get; }
}
public interface IDialogCloseable<T> : ICancelable
{
event DialogCloseEvent<T> DialogClose;
}