Binding MenuItem's IsChecked to TabItem's

2019-09-06 19:48发布

问题:

I have a list of tab items that have views dynamically added to them. Every time a user adds a view, a new tab item is created. I'm now trying to bind a menu to a tabcontrol's items so that a user can select from a menu which view is currently the active view.

My menu is bound as such:

<Menu Background="Transparent">
    <MenuItem Style="{StaticResource TabMenuButtonStyle}" ItemsSource="{Binding RelativeSource={RelativeSource FindAncestor, AncestorType={x:Type TabControl}}, Path=Items}" ItemContainerStyle="{StaticResource TabMenuItem}"></MenuItem>
</Menu>

This works fine and has the desired effect (each menu item is a listing of all the open tabs).

I have the following style that binds menu items to the IsSelected property of the tab items:

<Setter Property="IsChecked" Value="{Binding Path=IsSelected, Mode=TwoWay}" />

My problem is, this binding doesn't work. The binding error message is stating that it can't find the IsSelected property on the view object. I don't want it to use the specfic view, rather, I want it to look at the tab item that the view is currently bound to.

I've tried the following, but still get a binding error:

<Setter Property="IsChecked" Value="{Binding Path=IsSelected, Mode=TwoWay, RelativeSource={RelativeSource AncestorType=TabItem}}}" />

Which states that it can't find an ancestor of type TabItem for each menu item (which makes sense as the menu item's ancestors are not what it is bound to.)

Is there any way I can get access to the parent of the item that is coming in as a binding so I can bind to its properties?

Update:

Per Yadyn's advice, I decided to create a value converter and return tab items.

public object Convert(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture) {
        ItemCollection ic = (ItemCollection)value;
        List<TabItem> tabItems = new List<TabItem>();
        foreach (var obj in ic) {
            tabItems.Add((TabItem)obj);
        }
        return tabItems;
    }

This makes binding IsSelected to IsChecked work for static items (TabControls that have their tab items already created), but for the dynamically added views, the Convert method never gets called. It's like the TabControl is not sending out an update to binders of its items that something has changed. Here is how the MenuItem is wired up now:

<MenuItem Style="{StaticResource TabMenuButtonStyle}" ItemsSource="{Binding RelativeSource={RelativeSource FindAncestor, AncestorType={x:Type TabControl}}, Path=Items, Mode=OneWay, NotifyOnSourceUpdated=True,  Converter={StaticResource TabControlItemConverter}}" ItemContainerStyle="{StaticResource TabMenuItem}"></MenuItem>

回答1:

TabControl.Items will get you back the views, since that is what you've bound to your TabControl to have dynamic tab views.

Unfortunately, there isn't a property you can bind to on the TabControl directly that will get you a collection of the TabItems. These are actually the ItemContainers for each item in the Items bound collection.

What you might do is create a converter or something. You can try using myTabControl.ItemContainerGenerator.ContainerFromItem and pass in the view object to get back the actual TabItem that wraps it. Then your IsSelected binding will work.

You might consider binding directly to the TabControl itself instead of the Items property. Then the converter can easily do the above call to ContainerFromItem. You'll then have to return a List<TabItems> from the converter by enumerating the Items property yourself (calling ContainerFromItem for each).

Anyway, hopefully this gets you on the right track!



回答2:

Here is something simpler. Define a toplevel viewmodel that holds a collection of viewmodels representing the tabs and menuitems like so

//Not showing here the details of implementing INPC
public class MyCustomCompositeViewModel:INotifyPropertyChanged
{
  public ObservableCollection<CompositeViewItem>CompositeItems{get;set;}
  public CompositeViewItem SelectedItem{get;set;}
}

On the view, bind the tabitems to the CompositeItems collection and bind the selected tab item to the selectedItem. You can bind the MenuItems similarly

The compositeviewitem should provide properties like the name of the item (for display on the tab and menu), and perhaps the additional data the View needs for rendering. Hope this makes sense.



标签: wpf xaml mvvm