0
votes

I have two grids,

  1. With a TabControl that bind on a ObservableCollection and a ContextMenuItem that will close a Dynamically TabItem.

    <Grid>
        <TabControl Name="mainTabControl" IsSynchronizedWithCurrentItem="True"  ItemsSource="{Binding ObservableCollectionTabItems}" Background="White" Margin="10,0,0,0">
            <TabControl.ItemTemplate>
                <DataTemplate>
                    <TextBlock Text="{Binding Header}" >
                        <TextBlock.ContextMenu>
                            <ContextMenu>
                                <MenuItem Header="Close" Click="MenuItemCloseTab_Click">
                                </MenuItem>
                            </ContextMenu>
                        </TextBlock.ContextMenu>
                    </TextBlock>
                </DataTemplate>
            </TabControl.ItemTemplate>
        </TabControl>
    </Grid>
    
  2. A ListBox that show the name o my Controls.

    <Grid Background="#FFD61B1B">
        <ListBox x:Name="RightListBox" SelectionMode="Single" SelectionChanged="RightListBox_OnSelectionChanged" IsSynchronizedWithCurrentItem="true" Margin="10,0,0,0">
            <ListBox.ItemTemplate>
                <DataTemplate>
                    <Label Margin="10" Content="{Binding}"/>
                </DataTemplate>
            </ListBox.ItemTemplate>
        </ListBox>
    </Grid>
    

To add a new Tab in the Code Behind from MainWindow.xaml:

this._vmMainWindowTabControl.AddTab("ViewOne", "Test");

Who VMMainWindowTabControl.cs is a ViewModel that have: VMBase -> INotifyPropertyChanged Class, VMParentForViews is a empty ViewModel class and VMViewTypeOne another ViewModel who we set the header on a TabItem in the constructor.

public class VMMainWindowTabControl :VMBase
{
    private VMParentForViews vmParentForViews;

    public VMMainWindowTabControl()
    {
        ObservableCollectionTabItems = new ObservableCollection<VMParentForViews>();
    }

    public ObservableCollection<VMParentForViews> ObservableCollectionTabItems { get; set; }


    ///<summary>
    /// I'm trying to get controls to Tabitem with SelectedIndex but I have not success.
    /// </summary>
    //public int SelectedIndex
    //{
    //    get
    //    {
    //        ICollectionView collectionView = CollectionViewSource.GetDefaultView(this.ObservableCollectionTabItems);
    //        return collectionView.CurrentPosition;
    //    }
    //    set
    //    {
    //        ICollectionView collectionView = CollectionViewSource.GetDefaultView(this.ObservableCollectionTabItems);
    //        collectionView.MoveCurrentToPosition(value);
    //        OnPropertyChanged("SelectedIndex");
    //    }
    //}

    /// <summary>
    /// Adds the tab to the TabControl
    /// </summary>
    /// <param name="viewType">Type of the view.</param>
    /// <param name="header">Header of the TabItem.</param>
    public void AddTab(string viewType, string header)
    {
        if(viewType.Equals("ViewOne"))
        {
            vmParentForViews = new VMViewTypeOne(header);
            this.ObservableCollectionTabItems.Add(vmParentForViews);
        }

        // Set the new tab to be the current tab
        ICollectionView collectionView1 = CollectionViewSource.GetDefaultView(this.ObservableCollectionTabItems);

        if (collectionView1 != null)
        {
            collectionView1.MoveCurrentTo(vmParentForViews);
        }
    }

    /// <summary>
    /// Closes the tab item.
    /// </summary>
    public void CloseTabItem(Object sender)
    {
        VMParentForViews vmParentForViews = (sender as MenuItem).DataContext as VMParentForViews;
        this.ObservableCollectionTabItems.Remove(vmParentForViews);
    }

    public void AddElement(Object sender)
    {
        // How I can do this.
    }

}

My problem is that when I click on a ListBoxItem, I get the SelectedItem of this ListBox. But now I doesn't know how I can reference to the corresponding TabItem that I will add this control. This TabItem is saved on the ObservableCollection but I need the sender who clicked on the tab. Maybe the are other ways that I dind't googled well.

Here is a image to explain the treeview on View and ViewModels on my project. enter image description here

I'm trying with the SelectedIndex property from VMMainWindowTabControl without success to add a element to a TabItem.

VTabItem.xaml is only a canvas item that is shown on each TabItem.

Create new Tab and close this tab a working, many thanks to Nishant Rana with this two tutorials: Creating dynamic TabItem in WPF and Adding a Close Context Menu to TabItem in WPF

Thank you very much for all the help. Greetings and happy new year! :D

2

2 Answers

3
votes

I believe your MVVM is getting blured and you can do a lot more with binding.

I have put together an example of what I think you are after, using binding, templating and RoutedCommands to achieve the functionality you have said you are after.

It works like this...

There are 3 models in my example, MyModel1 to MyModel3 and they are all basically

public class MyModel1
{
    public string Header { get { return "One"; }}
}

with the header returning a different value for each model.

The ViewModel simple as well

public class MyViewModel : INotifyPropertyChanged { private object selectedItem;

    public MyViewModel()
    {
        this.AvailableItems = new Collection<Type>() { typeof(MyModel1), typeof(MyModel2), typeof(MyModel3) };
        this.Items = new ObservableCollection<object>();
    }

    public Collection<Type> AvailableItems { get; set; }

    public ObservableCollection<object> Items { get; set; }

    public void AddItem(Type type)
    {
        var item = Items.FirstOrDefault(i => i.GetType() == type);
        if (item == null)
        {
            item = Activator.CreateInstance(type);
            Items.Add(item);
        }

        SelectedItem = item;
    }

    internal void RemoveItem(object item)
    {
        var itemIndex = this.Items.IndexOf(item);
        if (itemIndex > 0)
        {
            SelectedItem = Items[itemIndex - 1];
        }
        else if (Items.Count > 1)
        {
            SelectedItem = Items[itemIndex + 1];                
        }

        Items.Remove(item);
    }

    public object SelectedItem
    {
        get { return selectedItem; }
        set
        {
            if (value != selectedItem)
            {
                selectedItem = value;
                OnPropertyChanged();
            }
        }
    }

    private void OnPropertyChanged([CallerMemberName]string propertyName = null)
    {
        var handler = PropertyChanged;
        if (handler != null)
        {
            handler.Invoke(this, new PropertyChangedEventArgs(propertyName));
        }
    }

    public event PropertyChangedEventHandler PropertyChanged;
}

The ListBox will bind to AvailableItems and the TabControl will bind to Items.

There are 3 UserControl's, one for each Model and they all look something like this

<UserControl x:Class="StackOverflow._20933056.UserControl1" 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" 
             mc:Ignorable="d" 
             d:DesignHeight="300" d:DesignWidth="300">
    <TextBlock Text="User Control 1" />
</UserControl>

The views code behind instantiates the ViewModel, registers a RoutedCommand and handles the events of the RoutedCommand.

public partial class MainWindow : Window
{
    public static RoutedCommand CloseItemCommand = new RoutedCommand("CloseItem", typeof(MainWindow));

    public MainWindow()
    {
        this.ViewModel = new MyViewModel();
        InitializeComponent();
    }

    public MyViewModel ViewModel { get; set; }

    private void MyListBox_OnSelectionChanged(object sender, SelectionChangedEventArgs e)
    {
        this.ViewModel.AddItem(e.AddedItems.OfType<Type>().FirstOrDefault());
    }

    private void CommandBinding_OnCanExecute(object sender, CanExecuteRoutedEventArgs e)
    {
        e.CanExecute = true;
        e.Handled = true;
    }

    private void CommandBinding_OnExecuted(object sender, ExecutedRoutedEventArgs e)
    {
        this.ViewModel.RemoveItem(e.Parameter);
    }

More on the RoutedCommand later.

The fun is in the Xaml, which is quite simple

<Window x:Class="StackOverflow._20933056.MainWindow" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:this="clr-namespace:StackOverflow._20933056"
        DataContext="{Binding RelativeSource={RelativeSource Self}, Path=ViewModel}"
        Title="MainWindow" Height="600" Width="800">
    <Window.Resources>
        <ContextMenu x:Key="TabContextMenu">
            <MenuItem Header="Close" Command="{x:Static this:MainWindow.CloseItemCommand}" CommandParameter="{Binding}" />
        </ContextMenu>
    <DataTemplate DataType="{x:Type this:MyModel1}">
        <this:UserControl1 DataContext="{Binding}" ContextMenu="{StaticResource TabContextMenu}" />
    </DataTemplate>
    <DataTemplate DataType="{x:Type this:MyModel2}">
        <this:UserControl2 DataContext="{Binding}" ContextMenu="{StaticResource TabContextMenu}" />
    </DataTemplate>
    <DataTemplate DataType="{x:Type this:MyModel3}">
        <this:UserControl2 DataContext="{Binding}" ContextMenu="{StaticResource TabContextMenu}" />
    </DataTemplate>
    <Style TargetType="{x:Type TabItem}">
        <Setter Property="Header" Value="{Binding Path=Header}" />
        <Setter Property="ContextMenu" Value="{StaticResource TabContextMenu}" />            
    </Style>
</Window.Resources>

<Window.CommandBindings>
    <CommandBinding Command="{x:Static this:MainWindow.CloseItemCommand}" CanExecute="CommandBinding_OnCanExecute" Executed="CommandBinding_OnExecuted" />
</Window.CommandBindings>
<Grid>
    <Grid>
        <Grid.ColumnDefinitions>
            <ColumnDefinition Width="200" />
            <ColumnDefinition />
        </Grid.ColumnDefinitions>

        <ListBox x:Name="MyListBox" ItemsSource="{Binding Path=AvailableItems}" SelectionChanged="MyListBox_OnSelectionChanged">
            <ListBox.ItemTemplate>
                <DataTemplate DataType="{x:Type system:Type}"><TextBlock Text="{Binding Path=Name}" /></DataTemplate>
            </ListBox.ItemTemplate>
        </ListBox>
            <TabControl Grid.Column="1" ItemsSource="{Binding Path=Items}" SelectedItem="{Binding Path=SelectedItem, Mode=TwoWay}" />
        </Grid>
    </Grid>
</Window>

As I said earlier, the ListBox binds to the AvailableItems of the ViewModel and the TabControl binds to the Items. The TabControl also binds to the SelectedItem which allows control of the selected tab from the view model.

The ListBox.SelectionChanged event is handled in the code behind to call the ViewModel.AddItem method which adds or selects a tab item.

NOTE: Each tab in the TabControl is actually a Model object, not a TabItem control. There are DataTemplates defined to allow the TabControl to correctly insert the required UserControl for each Model in the content of the TabItem.

TabItem management is via the AddItem and RemoveItem methods in the ViewModel.

Now, back to the RoutedCommand.

The RoutedCommand allows a command to be defined, which can be fired from somewhere in the VisualTree and then pick it up somewhere else, with out the receiving handler caring about where it came from.

So, in the Xaml, there is a ContextMenu resource called TabContextMenu. That resource is bound to the ContextMenu of all the TabItem's via a Style. It is also bound the ContextMenu of each UserControl in the DataTemplates.

In the ContextMenu has a MenuItem that will fire the RoutedCommand, passing the current DataContext (the Model) with it.

The MainWindow has a CommandBinding that receives and handles the RoutedCommand. In the CommandBindings Executed event handler, the ViewModel.RemoveItem method is called.

The code here is almost the complete code base of my example. Only the implementation of MyModel2, MyModel3, UserControl2 and UserControl3 are missing from this answer and they can be inferred from MyModel1 and UserControl1. You should be able to reproduce the example in a new C#/WPF project.

I hope this helps.

0
votes

Your example work perfectly! thank you, you give me more perspective about MVVM.

if I will update, change the AvailableItems from code behind. I proceed to do it but without success.

I change it from a Collection to a ObservableCollection:

public ObservableCollection<Type> AvailableItems { get; set; }

The Constructor I doesn't fill it.

public MyViewModel()
    {
        this.AvailableItems = new ObservableCollection<Type>();
        this.Items = new ObservableCollection<object>();
    }

Now to add a Tab:

private void AddTab(object sender, RoutedEventArgs e)
    {
        foreach (ClassFoo item in instanceBoo.methodBoo)
        {
            Type test = (Type) item.GetType();
            this.myViewModel.AvailableItems.Add(test);
        }
    }

My Listbox is:

<Grid Background="#FFD61B1B">
<ListBox x:Name="RightListBox" ItemsSource="{Binding Path=AvailableItems}" SelectionMode="Single" SelectionChanged="RightListBox_OnSelectionChanged" IsSynchronizedWithCurrentItem="true" Margin="10,0,0,0">
    <ListBox.ItemTemplate>
        <DataTemplate DataType="{x:Type system:Type}">
            <Label Margin="10" Content="{Binding Path=Name}"/>
        </DataTemplate>
    </ListBox.ItemTemplate>
</ListBox>

When I run the Application, I have Data in AvailableItems, but it isn't refresh the listbox. I try to change the AvailableItems to a local Collection that have IPropertyChanged, but doesn't work too.

private Collection<Type> availableItems;

public Collection<Type> AvailableItems
    {
        get { return availableItems; }
        set
        {
            if (value != availableItems)
            {
                availableItems = value;
                OnPropertyChanged();
            }
        } 
    }

My problem is that I must show first a blank tab. Next I must fill it with controls, these will be our AvailableItems, and after save it.

I Have a another question too,

If I want to delete a TabItem from a button?

<Button Content="Button"  Command="{x:Static mvvmTestStackOverflowAnswer1:MainWindow.CloseItemCommand}" CommandParameter="{Binding}" Grid.Column="1" HorizontalAlignment="Left" Height="46" Margin="478,10,0,0" VerticalAlignment="Top" Width="104"/>

Asuming the same structure that the ContextMenu to send the command to close. But itemIndex give me always a -1.

internal void RemoveItem(object item)
    {
        var itemIndex = this.Items.IndexOf(item);
        if (itemIndex > 0)
        {
            SelectedItem = Items[itemIndex - 1];
        }
        else if (Items.Count > 1)
        {
            SelectedItem = Items[itemIndex + 1];
        }

        this.Items.Remove(item);
    }

Thank's for all help! Greetings.