2009-03-19 7 views
17

Ich habe ein Problem beim Binden eines Befehls in einem Kontextmenü auf einem Benutzersteuerelement, das sich auf einer Registerkarte befindet. Das erste Mal, wenn ich das Menü benutze (rechte Maustaste auf die Registerkarte), funktioniert es großartig, aber wenn ich die Registerkarte wechsle, verwendet der Befehl die datenbankspezifische Instanz, die beim ersten Mal verwendet wurde.WPF-Kontextmenü bindet nicht an rechtes datengebundenes Element

Stecke ich eine Schaltfläche, die auf den Befehl in dem Usercontrol gebunden ist es wie erwartet funktioniert ...

Kann jemand bitte sagen Sie mir, was ich falsch mache ??

Dies ist ein Testprojekt, das das Problem aussetzt:

App.xaml.cs:

public partial class App : Application 
{ 
    protected override void OnStartup(StartupEventArgs e) 
    { 
     base.OnStartup(e); 

     CompanyViewModel model = new CompanyViewModel(); 
     Window1 window = new Window1(); 
     window.DataContext = model; 
     window.Show(); 
    } 
} 

Window1.xaml:

<Window x:Class="WpfApplication1.Window1" 
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" 
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" 
    xmlns:vw="clr-namespace:WpfApplication1" 
Title="Window1" Height="300" Width="300"> 

    <Window.Resources> 
    <DataTemplate x:Key="HeaderTemplate"> 
     <StackPanel Orientation="Horizontal"> 
      <TextBlock Text="{Binding Path=Name}" /> 
     </StackPanel> 
    </DataTemplate> 
    <DataTemplate DataType="{x:Type vw:PersonViewModel}"> 
     <vw:UserControl1/> 
    </DataTemplate> 

</Window.Resources> 
<Grid> 
    <TabControl ItemsSource="{Binding Path=Persons}" 
       ItemTemplate="{StaticResource HeaderTemplate}" 
       IsSynchronizedWithCurrentItem="True" /> 
</Grid> 
</Window> 

UserControl1.xaml:

<UserControl x:Class="WpfApplication1.UserControl1" 
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" 
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" 
    MinWidth="200"> 
    <UserControl.ContextMenu> 
     <ContextMenu > 
      <MenuItem Header="Change" Command="{Binding Path=ChangeCommand}"/> 
     </ContextMenu> 
    </UserControl.ContextMenu> 
    <Grid> 
     <Grid.ColumnDefinitions> 
      <ColumnDefinition Width="100" /> 
      <ColumnDefinition Width="*" /> 
     </Grid.ColumnDefinitions> 
     <Label Grid.Column="0">The name:</Label> 
     <TextBox Grid.Column="1" Text="{Binding Path=Name, UpdateSourceTrigger=PropertyChanged}" /> 
    </Grid> 
</UserControl> 

Komp nyViewModel.cs:

public class CompanyViewModel 
{ 
    public ObservableCollection<PersonViewModel> Persons { get; set; } 
    public CompanyViewModel() 
    { 
     Persons = new ObservableCollection<PersonViewModel>(); 
     Persons.Add(new PersonViewModel(new Person { Name = "Kalle" })); 
     Persons.Add(new PersonViewModel(new Person { Name = "Nisse" })); 
     Persons.Add(new PersonViewModel(new Person { Name = "Jocke" })); 
    } 
} 

PersonViewModel.cs:

public class PersonViewModel : INotifyPropertyChanged 
{ 
    Person _person; 
    TestCommand _testCommand; 

    public PersonViewModel(Person person) 
    { 
     _person = person; 
     _testCommand = new TestCommand(this); 
    } 
    public ICommand ChangeCommand 
    { 
     get 
     { 
      return _testCommand; 
     } 
    } 
    public string Name 
    { 
     get 
     { 
      return _person.Name; 
     } 
     set 
     { 
      if (value == _person.Name) 
       return; 
      _person.Name = value; 
      OnPropertyChanged("Name"); 
     } 
    } 
    void OnPropertyChanged(string propertyName) 
    { 
     PropertyChangedEventHandler handler = this.PropertyChanged; 
     if (handler != null) 
     { 
      var e = new PropertyChangedEventArgs(propertyName); 
      handler(this, e); 
     } 
    } 
    public event PropertyChangedEventHandler PropertyChanged; 
} 

TestCommand.cs:

public class TestCommand : ICommand 
{ 
    PersonViewModel _person; 
    public event EventHandler CanExecuteChanged; 

    public TestCommand(PersonViewModel person) 
    { 
     _person = person; 
    } 
    public bool CanExecute(object parameter) 
    { 
     return true; 
    } 
    public void Execute(object parameter) 
    { 
     _person.Name = "Changed by command"; 
    } 
} 

Person.cs:

public class Person 
{ 
    public string Name { get; set; } 
} 

Antwort

22

Der Schlüssel ist hier zu erinnern ist Kontext mich nus sind nicht Teil des visuellen Baumes.

Daher erben sie nicht die gleiche Quelle wie das Steuerelement, für das sie zum Binden gehören. Der Weg, um damit fertig zu werden, besteht darin, sich an das Placement-Ziel des ContextMenu selbst zu binden.

<MenuItem Header="Change" Command="{Binding 
    Path=PlacementTarget.ChangeCommand, 
    RelativeSource={RelativeSource FindAncestor, AncestorType={x:Type ContextMenu}}}" 
/> 
+0

Hallo Cameron. Glauben Sie, Ihre Technik hier irgendwie mit dem Problem ist ich hier beschrieben habe: http://stackoverflow.com/questions/833607/wpf-why-do-contextmenu-items-work-for-listbox-but-not- itemscontrol ... Ich bin nicht an einen Befehl gebunden, aber ich habe den Verdacht, dass es ein verwandtes Problem ist. –

+2

Ich bin nicht von dieser Antwort überzeugt. Die Befehlsbindungen funktionieren für den Menüeintrag (er weiß, dass er das Ansichtsmodell binden muss) ... das Problem besteht darin, dass die Menüelemente nicht neu binden, wenn sich der Datenkontext aufgrund des Wechselns der Registerkarte ändert. Wenn es wegen ihnen nicht Teil des visuellen Baumes ist, wie kommt es zum ersten Mal funktioniert? – Schneider

+0

@Schneider: Ich habe nicht gesagt, dass Bindungen in einem Menü nicht funktionieren, nur dass sie ihren Datenkontext nicht wie erwartet von ihren Elternkontexten erben. Ich würde sagen, dass die WPF-Bindungs-Engine den Kontext beim ersten Öffnen des Menüs einstellt und dann nicht aktualisiert, wenn sich die Registerkarte ändert. –

8

Der sauberste Weg, die ich gefunden habe Befehle Kontextmenüelemente binden, um eine Klasse beinhaltet die Verwendung genannt CommandReference. Sie finden es im MVVM-Toolkit unter Codeplex unter WPF Futures.

Die XAML könnte wie folgt aussehen:

<UserControl x:Class="View.MyView" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" 
       xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" 
       xmlns:vm="clr-namespace:ViewModel;assembly=MyViewModel" 
       xmlns:mvvm="clr-namespace:ViewModelHelper;assembly=ViewModelHelper" 
      <UserControl.Resources> 
       <mvvm:CommandReference x:Key="MyCustomCommandReference" Command="{Binding MyCustomCommand}" /> 

       <ContextMenu x:Key="ItemContextMenu"> 
        <MenuItem Header="Plate"> 
         <MenuItem Header="Inspect Now" Command="{StaticResource MyCustomCommandReference}" 
           CommandParameter="{Binding}"> 
         </MenuItem> 
        </MenuItem> 
       </ContextMenu> 
    </UserControl.Resources> 

MyCustomCommand ist ein RelayCommand auf der Ansichtsmodell. In diesem Beispiel wurde das ViewModel an den Datenkontext der Ansicht im CodeBehind angehängt.

Hinweis: Dieser XAML wurde aus einem Arbeitsprojekt kopiert und zur Veranschaulichung vereinfacht. Es kann Tippfehler oder andere kleinere Fehler geben.

+1

Haben Sie dies mit einem RelayCommand mit einem CanExecute-Delegaten versucht, CyberMonk? Ich habe festgestellt, dass CommandReference an den Parameter CanExecute null übergeben wird, obwohl die Execute-Methode den richtigen Wert übergeben wird. Es hindert mich daran, es jetzt zu benutzen. –

+0

OK, das funktioniert vielleicht, aber kann jemand erklären, warum es benötigt wird? Warum werden Bindungen in ContextMenus nur einmal ausgeführt? – Schneider

+0

Ich kann das überprüfen funktioniert ... Erklärungen sind willkommen :) – Schneider

0

fand ich diese Methode mit der Tag-Eigenschaft sehr nützlich, wenn aus einem Kontextmenü tief im Innern einer Steuervorlage Bindung:

http://blog.jtango.net/binding-to-a-menuitem-in-a-wpf-context-menu

Dies macht es möglich, zu binden, zu jedem Datenkontext, der für das Steuerelement verfügbar ist, von dem aus das Kontextmenü geöffnet wurde. Das Kontextmenü kann über "PlacementTarget" auf das angeklickte Steuerelement zugreifen. Wenn die Tag-Eigenschaft des angeklickten Steuerelements an einen gewünschten Datenkontext gebunden ist, führt die Bindung an "PlacementTarget.Tag" aus dem Kontextmenü direkt zu diesem Datenkontext.

1

ziehe ich eine andere Lösung. Hinzufügen Kontextmenü Ereignis Loader.

<ContextMenu Loaded="ContextMenu_Loaded"> 
    <MenuItem Header="Change" Command="{Binding Path=ChangeCommand}"/> 
</ContextMenu> 

Datenkontext innerhalb des Ereignisses zuweisen.

4

Ich hatte das gleiche Problem in letzter Zeit mit einem ContextMenu in einer ListBox. Ich habe versucht, einen Befehl der MVVM-Art ohne Code-Behind zu binden. Ich gab schließlich auf und ich bat einen Freund um seine Hilfe. Er fand eine leicht verdrehte, aber präzise Lösung. Er übergibt die ListBox im DataContext des ContextMenu und sucht dann den Befehl im View-Modell, indem er auf den DataContext der ListBox zugreift. Dies ist die einfachste Lösung, die ich bisher gesehen habe. Kein benutzerdefinierter Code, kein Tag, nur reines XAML und MVVM.

gab ich ein voll funktionstüchtiges Beispiel auf Github. Hier ist ein Auszug aus dem XAML.

<Window x:Class="WpfListContextMenu.MainWindow" 
     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" 
     Title="MainWindow" Height="350" Width="268"> 
    <Grid> 
    <DockPanel> 
     <ListBox x:Name="listBox" DockPanel.Dock="Top" ItemsSource="{Binding Items}" DisplayMemberPath="Name" 
       SelectionMode="Extended"> 
     <ListBox.ContextMenu> 
      <ContextMenu DataContext="{Binding Path=PlacementTarget, RelativeSource={RelativeSource Self}}"> 
      <MenuItem Header="Show Selected" Command="{Binding Path=DataContext.ShowSelectedCommand}" 
         CommandParameter="{Binding Path=SelectedItems}" /> 
      </ContextMenu> 
     </ListBox.ContextMenu> 
     </ListBox> 
    </DockPanel> 
    </Grid> 
</Window> 
0

Ich weiß, das bereits eine alte Post, aber ich möchte eine andere Lösung für diejenigen ein hinzuzufügen, die für verschiedene Möglichkeiten suchen, es zu tun.

Ich konnte nicht die gleiche Lösung in meinem Fall arbeiten, da ich versuchte, etwas anderes zu tun: Öffnen Sie das Kontextmenü mit einem Mausklick (genauso wie eine Symbolleiste mit einem Untermenü) und binden Sie auch Befehle zu meinem Modell. Da ich einen Ereignisauslöser verwendet habe, war das PlacementTarget-Objekt null.

Dies ist die Lösung, die ich fand es nur funktioniert XAML:

<!-- This is an example with a button, but could be other control --> 
<Button> 
    <...> 

    <!-- This opens the context menu and binds the data context to it --> 
    <Button.Triggers> 
    <EventTrigger RoutedEvent="Button.Click"> 
     <EventTrigger.Actions> 
     <BeginStoryboard> 
      <Storyboard> 
      <ObjectAnimationUsingKeyFrames Storyboard.TargetProperty="ContextMenu.DataContext"> 
       <DiscreteObjectKeyFrame KeyTime="0:0:0" Value="{Binding}"/> 
      </ObjectAnimationUsingKeyFrames> 
      <BooleanAnimationUsingKeyFrames Storyboard.TargetProperty="ContextMenu.IsOpen"> 
       <DiscreteBooleanKeyFrame KeyTime="0:0:0" Value="True"/> 
      </BooleanAnimationUsingKeyFrames> 
      </Storyboard> 
     </BeginStoryboard> 
     </EventTrigger.Actions> 
    </EventTrigger> 
    </Button.Triggers> 

    <!-- Here it goes the context menu --> 
    <Button.ContextMenu> 
    <ContextMenu> 
     <MenuItem Header="Item 1" Command="{Binding MyCommand1}"/> 
     <MenuItem Header="Item 2" Command="{Binding MyCommand2}"/> 
    </ContextMenu> 
    </Button.ContextMenu> 

</Button>