2012-04-28 10 views
7

que tienen este recurso menú contextual:¿Cómo implementar comandos para usar métodos ancestrales en WPF?

<ResourceDictionary xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" 
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"> 
    <ContextMenu x:Key="FooContextMenu"> 
     <ContextMenu.CommandBindings> 
      <CommandBinding Command="Help" Executed="{Binding ElementName=MainTabs, Path=HelpExecuted}" /> 
     </ContextMenu.CommandBindings> 

     <MenuItem Command="Help"> 
      <MenuItem.Icon> 
       <Image Source="../Resources/Icons/Help.png" Stretch="None" /> 
      </MenuItem.Icon> 
     </MenuItem> 
    </ContextMenu> 
</ResourceDictionary> 

Quiero volver a utilizarlo en dos lugares. En primer lugar estoy tratando de ponerlo en un DataGrid:

<DataGrid ContextMenu="{DynamicResource FooContextMenu}">... 

El ContextMenu en sí funciona bien, pero con la Executed="..." ahora he rompe la aplicación y los tiros:

Una primera oportunidad de tipo 'System.InvalidCastException' ocurrió en PresentationFramework.dll

información adicional: No se puede convertir objeto de tipo 'System.Reflection.RuntimeEventInfo' para escribir 'System.Reflection.MethodInfo'.

Si elimino toda la definición Executed="...", el código funciona (y el comando no hace nada/está atenuado). La excepción se produce tan pronto como hago clic con el botón derecho en la cuadrícula/abro el menú contextual.

El DataGrid se coloca debajo de unos pocos elementos, pero al final todos ellos se encuentran por debajo de un TabControl (llamado MainTabs) que tiene ItemsSource conjunto a una colección de FooViewModel s, y en ese FooViewModel tengo un método HelpExecuted la que quiero estar llamado.

Vamos a visualizar:

  • TabControl (ItemsSource=ObservableCollection<FooViewModel>, x:Name=MainTabs)
    • cuadrícula
      • Más IU
        • cuadrícula de datos (con juego menú contextual)

¿Por qué recibo este error y cómo puedo hacer que el comando del menú contextual para "objetivo" del FooViewModel 's HelpExecuted método?

+0

creo ese control de MainTabs no contiene una propiedad HelpExecuted. Contiene solo una lista de FooViewModel. –

Respuesta

3

Desafortunadamente no puede enlazar Executed para un ContextMenu ya que es un evento. Un problema adicional es que el ContextMenu no existe en el VisualTree existe el resto de su aplicación. Hay soluciones para ambos problemas.

En primer lugar, puede utilizar la propiedad Tag del control principal del ContextMenu para transferir el DataContext de su aplicación. Luego puede usar un DelegateCommand para su CommandBinding y listo. Aquí hay una pequeña muestra que muestra View, ViewModel y la implementación DelegateCommand que debería agregar a su proyecto.

DelegateCommand.cs

public class DelegateCommand : ICommand 
{ 
    private readonly Action<object> execute; 
    private readonly Predicate<object> canExecute; 

    public DelegateCommand(Action<object> execute) 
     : this(execute, null) 
    { } 

    public DelegateCommand(Action<object> execute, Predicate<object> canExecute) 
    { 
     if (execute == null) 
      throw new ArgumentNullException("execute"); 

     this.execute = execute; 
     this.canExecute = canExecute; 
    } 

    #region ICommand Members 

    [DebuggerStepThrough] 
    public bool CanExecute(object parameter) 
    { 
     return canExecute == null ? true : canExecute(parameter); 
    } 

    public event EventHandler CanExecuteChanged 
    { 
     add { CommandManager.RequerySuggested += value; } 
     remove { CommandManager.RequerySuggested -= value; } 
    } 

    public void Execute(object parameter) 
    { 
     execute(parameter); 
    } 

    #endregion 
} 

MainWindowView.xaml

<Window x:Class="Application.MainWindowView" 
     xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" 
     xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" 
     Title="MainWindowView" Height="300" Width="300" 
     x:Name="MainWindow"> 
    <Window.Resources> 
     <ResourceDictionary> 
      <ContextMenu x:Key="FooContextMenu"> 
       <MenuItem Header="Help" Command="{Binding PlacementTarget.Tag.HelpExecuted, RelativeSource={RelativeSource AncestorType=ContextMenu}}" /> 
      </ContextMenu> 
     </ResourceDictionary> 
    </Window.Resources> 
    <Grid> 
     <TabControl ItemsSource="{Binding FooViewModels}" x:Name="MainTabs"> 
      <TabControl.ContentTemplate> 
       <DataTemplate> 
        <DataGrid ContextMenu="{DynamicResource FooContextMenu}" Tag="{Binding}" /> 
       </DataTemplate> 
      </TabControl.ContentTemplate> 
     </TabControl> 
    </Grid> 
</Window> 

MainWindowView.xaml.cs

public partial class MainWindowView : Window 
{ 
    public MainWindowView() 
    { 
     InitializeComponent(); 
     DataContext = new MainWindowViewModel(); 
    } 
} 

MainWindowViewModel.cs

public class MainWindowViewModel 
{ 
    public ObservableCollection<FooViewModel> FooViewModels { get; set; } 

    public MainWindowViewModel() 
    { 
     FooViewModels = new ObservableCollection<FooViewModel>(); 
    } 
} 

FooViewModel.cs

public class FooViewModel 
{ 
    public ICommand HelpExecuted { get; set; } 

    public FooViewModel() 
    { 
     HelpExecuted = new DelegateCommand(ShowHelp); 
    } 

    private void ShowHelp(object obj) 
    { 
     // Yay! 
    } 
} 
+0

¿Las etiquetas y los delegados son realmente el camino a seguir? 'Stipo' tenía una forma corta y de trabajo para lograr esto, ¿tiene este enfoque suyo algo que' Stipo' no tiene? Estoy tratando de descubrir qué enfoque tiene más sentido, porque las respuestas que aquí se dan son bastante diferentes. :) – Tower

+0

De hecho, la respuesta de Stipo debería resolver su problema. Pero si aprendí una cosa sobre WPF y MVVM: Nunca use código subyacente si no tiene una muy buena razón para hacerlo, ya que anula todo el patrón.Esto a veces significa ir de una manera muy rocosa :) – MatthiasG

+0

No estoy de acuerdo con MatthiasG en que el código subyacente anula el patrón de MVVM. Lo más importante en MVVM es que toda la lógica y el estado de la aplicación deben estar en el modelo de vista y que no hay referencias directas a las vistas de los modelos de vista. La vinculación de la vista y el modelo de vista se debe hacer a través de XAML, si es posible (si la API WPF/XAML lo admite); de lo contrario, debe hacerse a través del código, como en mi respuesta. Si las vinculaciones a través del código son repetitivas, se pueden hacer XAMLish con comportamientos adjuntos. – Stipo

3

¿Le sirve de ayuda?

<ContextMenu> 
    <ContextMenu.ItemContainerStyle> 
     <Style TargetType="MenuItem"> 
      <Setter Property="Command" Value="{Binding RelativeSource={RelativeSource AncestorType={x:Type TabItem}}, Path=HelpExecuted}" /> 
     </Style> 
    </ContextMenu.ItemContainerStyle> 
    <MenuItem Header="Help" /> 
</ContextMenu> 
2

que está recibiendo este error porque CommandBinding.Executed no es propiedad de dependencia de lo que no puede unirse a ella.

En su lugar, use el código ResourceDictionary detrás para especificar controlador de eventos para el evento CommandBinding.Executed, y en el llamado código de controlador de eventos FooViewModel.HelpExecuted() de la siguiente manera:

MainWindowResourceDictionary.xaml

<ResourceDictionary x:Class="WpfApplication.MainWindowResourceDictionary" 
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" 
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" 
        xmlns:local="clr-namespace:WpfApplication"> 

    <DataTemplate DataType="{x:Type local:FooViewModel}"> 
     <Grid> 
      <DataGrid ContextMenu="{DynamicResource FooContextMenu}"/> 
     </Grid> 
    </DataTemplate> 

    <ContextMenu x:Key="FooContextMenu"> 
     <ContextMenu.CommandBindings> 
      <CommandBinding Command="Help" Executed="HelpExecuted"/> 
     </ContextMenu.CommandBindings> 
     <MenuItem Command="Help"/> 
    </ContextMenu> 

</ResourceDictionary> 

MainWindowResourceDictionary.xaml.cs

public partial class MainWindowResourceDictionary : ResourceDictionary 
{ 
    public MainWindowResourceDictionary() 
    { 
     InitializeComponent(); 
    } 

    private void HelpExecuted(object sender, ExecutedRoutedEventArgs e) 
    { 
     var fooViewModel = (FooViewModel)((FrameworkElement)e.Source).DataContext; 
     fooViewModel.HelpExecuted(); 
    } 
} 
+0

¡Esto funciona muy bien! ¿Crees que hay algún inconveniente en este enfoque? Todas las respuestas que tengo hasta ahora son bastante diferentes, parece que no hay una manera definida de hacer esto ...? – Tower

+0

Mi enfoque realmente usa el comando ApplicationsCommands.Help (http://msdn.microsoft.com/en-us/library/system.windows.input.applicationcommands.help.aspx), en el cual obtienes Key Gesture y UI Text por defecto gratis. Otro beneficio de usar ApplicationCommands es que son comandos bien conocidos (expuestos a través de WPF API) para que los autores de control puedan usarlos en sus controles sin saber quién los maneja (quién proporciona la implementación real de la lógica de comando). Otros enfoques no usan ApplicationCommands y complican innecesariamente el código transfiriendo DataContext a través de Tag. – Stipo

+0

Esto no parece funcionar con comandos personalizados? – Tower

3

Me temo MatthiasG se me adelantó. Mi solución es similar:

Aquí el comando de Ayuda es manejado por el modelo de vista del elemento de tabulación. Sería simple pasar una referencia al TestViewModel a cada uno de TestItemViewModel y hacer que ShowHelp vuelva a llamar a TestViewModel si es necesario.

public class TestViewModel 
{ 
    public TestViewModel() 
    { 
     Items = new List<TestItemViewModel>{ 
        new TestItemViewModel(), new TestItemViewModel() }; 
    } 

    public ICommand HelpCommand { get; private set; } 

    public IList<TestItemViewModel> Items { get; private set; } 
} 

public class TestItemViewModel 
{ 
    public TestItemViewModel() 
    { 
     // Expression Blend ActionCommand 
     HelpCommand = new ActionCommand(ShowHelp); 
     Header = "header"; 
    } 

    public ICommand HelpCommand { get; private set; } 

    public string Header { get; private set; } 

    private void ShowHelp() 
    { 
     Debug.WriteLine("Help item"); 
    } 
} 

El xaml

<Window.Resources> 
    <ContextMenu x:Key="FooMenu"> 
     <MenuItem Header="Help" Command="{Binding HelpCommand}"/> 
    </ContextMenu> 
    <DataTemplate x:Key="ItemTemplate"> 
     <!-- context menu on header --> 
     <TextBlock Text="{Binding Header}" ContextMenu="{StaticResource FooMenu}"/> 
    </DataTemplate> 
    <DataTemplate x:Key="ContentTemplate"> 
     <Grid Background="#FFE5E5E5"> 
      <!-- context menu on data grid --> 
      <DataGrid ContextMenu="{StaticResource FooMenu}"/> 
     </Grid> 
    </DataTemplate> 
</Window.Resources> 

<Window.DataContext> 
    <WpfApplication2:TestViewModel/> 
</Window.DataContext> 

<Grid> 
    <TabControl 
     ItemsSource="{Binding Items}" 
     ItemTemplate="{StaticResource ItemTemplate}" 
     ContentTemplate="{StaticResource ContentTemplate}" /> 
</Grid> 

modelos de vista alternativos para que el comando de ayuda se dirige al modelo de vista raíz

public class TestViewModel 
{ 
    public TestViewModel() 
    { 
     var command = new ActionCommand(ShowHelp); 

     Items = new List<TestItemViewModel> 
        { 
         new TestItemViewModel(command), 
         new TestItemViewModel(command) 
        }; 
    } 

    public IList<TestItemViewModel> Items { get; private set; } 

    private void ShowHelp() 
    { 
     Debug.WriteLine("Help root"); 
    } 
} 

public class TestItemViewModel 
{ 
    public TestItemViewModel(ICommand helpCommand) 
    { 
     HelpCommand = helpCommand; 
     Header = "header"; 
    } 

    public ICommand HelpCommand { get; private set; } 

    public string Header { get; private set; } 
} 

Una muy simple aplicación de ActionCommand

public class ActionCommand : ICommand 
{ 
    private readonly Action _action; 

    public ActionCommand(Action action) 
    { 
     if (action == null) 
     { 
      throw new ArgumentNullException("action"); 
     } 

     _action = action; 
    } 

    public bool CanExecute(object parameter) 
    { 
     return true; 
    } 

    public void Execute(object parameter) 
    { 
     _action(); 
    } 

    // not used 
    public event EventHandler CanExecuteChanged; 
} 
+0

No encuentra 'ActionCommand'. ¿Que es esto? – Tower

+0

ActionCommand está disponible después de instalar Expression Blend. Debería agregar una referencia a Microsoft.Expression.Interactions. Alternativamente, utilice un DelegateCommand de PRISM o un RelayCommand de MVVM light, o implemente el suyo. – Phil

1

I Es posible crear una clase de adaptador que se puede configurar como un recurso en XAML, se puede adjuntar a un Control para crear allí Comandos, y en el otro extremo se puede vincular a un método en el Modelo de Vista que se debe invocar cuando el comando se activa mediante un botón o elemento de menú. El comando en este caso sería un RoutedCommand, y no importaría si elige uno de los comandos WPF predefinidos o si crea un RoutedCommand personalizado en su aplicación.

El truco para unirse a un método es

  • Haciendo el adaptador de un congelar, por lo que uno puede utilizar la corriente DataContext como un origen de enlace,
  • dándole un DependencyProperty del tipo de delegado o uno de sus subtipos, y
  • Usando un convertidor que acepta el nombre del método como un parámetro convertidor e inspecciona el tipo de fuentes de enlace para crear un delegado para el método que debe invocarse mediante el comando.

Si bien esto suena complejo, lo bueno es que una vez que las partes del marco juntos, sólo tiene que volver a utilizar en XAML solamente, y que no tendrá ningún código de unión en absoluto, ya sea en o modelo de vista código detrás.

Como se puede imaginar, esto requiere algo de infraestructura, y el código es más de lo que me gustaría publicar aquí. Sin embargo, acabo de publicar un artículo en mi blog sobre el tema, http://wpfglue.wordpress.com/2012/05/07/commanding-binding-controls-to-methods/, y a través del blog puede descargar el código fuente completo para el marco y un ejemplo en VB.Net.

Aplicado a su problema, el XAML sería el siguiente aspecto:

En la definición de la contextMenu

<ResourceDictionary xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" 
       xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"> 
<ContextMenu x:Key="FooContextMenu"> 
    <!-- No CommandBindings needed here --> 
    <MenuItem Command="Help"> 
     <MenuItem.Icon> 
      <Image Source="../Resources/Icons/Help.png" Stretch="None" /> 
     </MenuItem.Icon> 
    </MenuItem> 
</ContextMenu> 
</ResourceDictionary> 

Y en la definición de la cuadrícula de datos

<DataGrid c:Commanding.CommandSet="{DynamicResource helpCommand}"> 
    <DataGrid.Resources> 
     <f:ActionConverter x:Key="actionConverter"/> 
     <c:ActionCommand x:Key="helpCommand" Command="Help" ExecutedAction="{Binding Converter={StaticResource actionConverter}, ConverterParameter=HelpExecuted}"/> 
<!-- DataGrid definition continued... --> 
Cuestiones relacionadas