Возможно ли открыть публичное событие из моего ViewModel таким образом, чтобы он мог привязываться к пользовательскому DependencyProperty в моем представлении?
Мое приложение написано на С# с использованием платформы.NET 4.5. Он имеет архитектуру MVVM без кода в представлении и пользовательские классы DependencyProperty для привязки специфических для WPF структур видимости свойств View, открытых ViewModel.
Существует набор свойств, которые я бы хотел, чтобы ViewModel мог показывать, представляющие события, на которые View должен отвечать. Например, когда объект ViewModel верхнего уровня будет удален, я бы хотел, чтобы реализация WPF View ответила, закрыв соответствующее окно. Это может произойти, когда процесс конфигурации отобразил диалоговое окно, пользователь включил и подтвердил информацию, и ViewModel передал ее в Модель и больше не требуется.
Я знаю, что есть много вопросов, которые являются специфическими для решения вопроса "показать диалог из ViewModel"; это не один из них, и у меня есть решение для этого.
Я прочитал документацию MSDN для DependencyProperties и не могу найти ничего конкретного для привязки к свойствам событий.
То, что я хотел бы достичь, похоже на код ниже. Этот код строит, но приводит к типичной System.Windows.Data Error: 40: BindingExpression path error: 'RequestCloseEvent' property not found
при отображении MainWindow.
Я знаю, что есть много вопросов, которые идут по строкам: "Пожалуйста, помогите мне отладить мою ошибку System.Windows.Data: 40 issue"; это (возможно) и не одно из них. (Но я был бы счастлив, если бы все это было на самом деле.)
Источник для настраиваемого DependencyProperty в WindowBindableProperties.cs:
using System;
using System.Threading;
using System.Windows;
namespace WpfEventBinding
{
public static class WindowBindableProperties
{
#region ViewModelTerminatingEventProperty
/// <summary>
/// Register the ViewModelTerminatingEvent custom DependencyProperty.
/// </summary>
private static DependencyProperty _viewModelTerminatingEventProperty =
DependencyProperty.RegisterAttached
(
"ViewModelTerminatingEvent",
typeof(ViewModelTerminatingEventHandler),
typeof(WindowBindableProperties),
new PropertyMetadata(null, ViewModelTerminatingEventPropertyChanged)
);
/// <summary>
/// Identifies the ViewModelTerminatingEvent dependency property.
/// </summary>
public static DependencyProperty ViewModelTerminatingEventProperty
{ get { return _viewModelTerminatingEventProperty; } }
/// <summary>
/// Gets the attached ViewModelTerminatingEvent dependecy property.
/// </summary>
/// <param name="dependencyObject">The window attached to the WindowViewModel.</param>
/// <returns>The ViewModelTerminatingEventHandler bound to this property</returns>
public static ViewModelTerminatingEventHandler GetViewModelTerminatingEvent
(DependencyObject dependencyObject)
{
return (dependencyObject.GetValue(ViewModelTerminatingEventProperty)
as ViewModelTerminatingEventHandler);
}
/// <summary>
/// Sets the ViewModelTerminatingEvent dependency property.
/// </summary>
public static void SetViewModelTerminatingEvent(
DependencyObject dependencyObject,
ViewModelTerminatingEventHandler value)
{
dependencyObject.SetValue(ViewModelTerminatingEventProperty, value);
}
/// <summary>
/// Gets the ViewModelTerminatingEvent dependency property.
/// </summary>
private static void ViewModelTerminatingEventPropertyChanged(
DependencyObject d,
DependencyPropertyChangedEventArgs e)
{
Window instance = d as Window;
if (null != instance)
{
if (null != e.OldValue)
{
throw new System.InvalidOperationException(
"ViewModelTerminatingEvent dependency property cannot be changed.");
}
if (null != e.NewValue)
{
// Attach the Window.Close() method to the ViewModel event
var newEvent = (e.NewValue as ViewModelTerminatingEventHandler);
newEvent += new ViewModelTerminatingEventHandler(() => instance.Close());
}
}
}
#endregion
}
}
Источник для MainWindow.xaml: (Этот пример содержит код для упрощения реализации кнопки Stop.)
<Window x:Class="WpfEventBinding.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:v="clr-namespace:WpfEventBinding"
v:WindowBindableProperties.ViewModelTerminatingEvent="{Binding Path=RequestCloseEvent}"
Title="MainWindow" Height="350" Width="525">
<Grid>
<Button Content="{Binding Path=CloseCommandName}" Click="StopButton_Click" ></Button>
</Grid>
</Window>
Источник для MainWindow.xaml.cs (код позади):
using System.Windows;
namespace WpfEventBinding
{
public partial class MainWindow : Window
{
public MainWindow()
{
InitializeComponent();
}
private void StopButton_Click(object sender, RoutedEventArgs e)
{
MainWindowViewModel vm = (DataContext as MainWindowViewModel);
if (null != vm)
{
vm.Stop();
}
}
}
}
Источник для MainWindowViewModel.cs:
using System;
using System.ComponentModel;
namespace WpfEventBinding
{
public delegate void ViewModelTerminatingEventHandler();
class MainWindowViewModel
: INotifyPropertyChanged
{
public event PropertyChangedEventHandler PropertyChanged;
// Raised by the ViewModel to indicate to the view that it is no longer required.
// Causes System.Windows.Data Error: 40 : BindingExpression path error. Is it
// Possible to bind to an 'event' property?
public event ViewModelTerminatingEventHandler RequestCloseEvent;
// This has to have the public 'get' to allow binding. Is there some way to
// do the same thing for the 'event'?
public String CloseCommandName { get; private set; }
public MainWindowViewModel()
{
CloseCommandName = "Close";
}
internal void Stop()
{
ViewModelTerminatingEventHandler RaiseRequestCloseEvent =
RequestCloseEvent;
if (null != RaiseRequestCloseEvent)
{
RaiseRequestCloseEvent();
}
}
internal void Start()
{
OnPropertyChanged("CloseCommandName");
OnPropertyChanged("ViewModelTerminatingEvent");
}
private void OnPropertyChanged(String propertyName)
{
PropertyChangedEventHandler RaisePropertyChangedEvent = PropertyChanged;
if (RaisePropertyChangedEvent != null)
{
var propertyChangedEventArgs = new PropertyChangedEventArgs(propertyName);
RaisePropertyChangedEvent(this, propertyChangedEventArgs);
}
}
}
}
Источник для App.xaml:
<Application x:Class="WpfEventBinding.App"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
>
<Application.Resources>
<!-- Nothing to see here. Move along... -->
</Application.Resources>
</Application>
Источник для App.xaml.cs
using System.Windows;
namespace WpfEventBinding
{
public partial class App : Application
{
public App()
{
Startup += new StartupEventHandler(App_Startup);
}
void App_Startup(object sender, StartupEventArgs e)
{
MainWindowViewModel vm = new MainWindowViewModel();
MainWindow window = new MainWindow();
// Make sure this is set before attempting binding!
window.DataContext = vm;
vm.Start();
window.Show();
}
}
}
Похоже, что public event ViewModelTerminatingEventHandler RequestCloseEvent;
синтаксиса недостаточно для allo привязки данных. Аналогичная проблема возникает, если public String CloseCommandName { get; private set; }
public String CloseCommandName { get; private set; }
public String CloseCommandName { get; private set; }
объявляется как public String CloseCommandName;
без { get; private set; }
{ get; private set; }
{ get; private set; }
. Однако нет { get; private set; }
{ get; private set; }
{ get; private set; }
для событий, которые используют {add{} remove{}}
синтаксис (и это также не решает проблему).
Я пытаюсь сделать это, и если да, то что я пропустил?
Просмотр закрытия означает событие закрытия окна. Поэтому вы в основном хотите реагировать на события в представлении. Я недавно прочитал эту статью, было очень хорошее изображение
а также упоминается существование EventBehavior
.
Лучше всего, если вы не хотите, чтобы какой-либо код позади, это использование поведения. Поведение - это простое приложенное свойство, которое может выполнять действия, например, растущие команды приложения, которые ViewModel
может затем захватывать без проблем MVVM
.
Вот пример поведения:
public static class FreezeBehavior
{
public static bool GetIsFrozen(DependencyObject obj)
{
return (bool)obj.GetValue(IsFrozenProperty);
}
public static void SetIsFrozen(DependencyObject obj, bool value)
{
obj.SetValue(IsFrozenProperty, value);
}
public static readonly DependencyProperty IsFrozenProperty =
DependencyProperty.RegisterAttached("IsFrozen", typeof(bool), typeof(FreezeBehavior), new PropertyMetadata(OnIsFrozenChanged));
private static void OnIsFrozenChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
if ((bool)e.NewValue)
{
var freezable = d as Freezable;
if (freezable != null && freezable.CanFreeze)
freezable.Freeze();
}
}
}
он использовался так
<DropShadowEffect ShadowDepth="2" local:FreezeBehavior.IsFrozen="True"/>
Он может быть прикреплен к любому замораживаемому, чтобы заморозить его. В вашем случае вы хотите подписаться на событие и вызвать команду или установить свойство или что угодно, чтобы сообщить ViewModel
.
То, о чем вы просите, это странно, но я не собираюсь подробно обсуждать это....
Вы не привязываетесь к событиям - вы их раскрываете, а представление может добавлять обработчики для событий.
Конечно, это означает, что вам нужно будет поместить некоторый код в представление - но это прекрасно, если он связан с пользовательским интерфейсом. Чтобы завершить развязку, ваш взгляд должен обрабатывать только viewmodel как интерфейс, это означает, что вы можете легко поменять местами модели просмотра на более позднем этапе.
(Обратите внимание, что я избегал говорить о триггерах событий).
Boolean
и событиеPropertyChanged
уже является частью фреймворка. К сведению: в моем приложении есть еще один DependencyProperty, который позволяет ViewModel подписаться на событие Closed из окна. Это механизм, который позволяет ViewModel указывать представлению, что окно больше не требуется и может быть закрыто.