0

I'm slowly learning MVVM in WPF. I code a GBA game editor. The editor consist of a main window (Editor.xaml) and depending which editor is selected in the menu, I'd like to display the corresponding persistent view (that is not destroyer when switching).

I'm trying to get working the TabControlEx class found in a few posts on SO such as here and here.

However I run into two problem: first, tabControlEx selectedItem does not change (fixed see edit) and second it seems on the TabItem OnMouseOver I lose the View (having a color Background property for the TabItem will switch to white color).

Now I'm pretty sure I'm missing something more or less obvious, but being new to MVVM I don't really know where to look. So here the code breakdown.

TabControlEx.cs

[TemplatePart(Name = "PART_ItemsHolder", Type = typeof(Panel))]
public class TabControlEx : System.Windows.Controls.TabControl
{
    // Holds all items, but only marks the current tab's item as visible
    private Panel _itemsHolder = null;

    // Temporaily holds deleted item in case this was a drag/drop operation
    private object _deletedObject = null;

    public TabControlEx()
        : base()
    {
        // this is necessary so that we get the initial databound selected item
        this.ItemContainerGenerator.StatusChanged += ItemContainerGenerator_StatusChanged;
    }

    /// <summary>
    /// if containers are done, generate the selected item
    /// </summary>
    /// <param name="sender"></param>
    /// <param name="e"></param>
    void ItemContainerGenerator_StatusChanged(object sender, EventArgs e)
    {
        if (this.ItemContainerGenerator.Status == GeneratorStatus.ContainersGenerated)
        {
            this.ItemContainerGenerator.StatusChanged -= ItemContainerGenerator_StatusChanged;
            UpdateSelectedItem();
        }
    }

    /// <summary>
    /// get the ItemsHolder and generate any children
    /// </summary>
    public override void OnApplyTemplate()
    {
        base.OnApplyTemplate();
        _itemsHolder = GetTemplateChild("PART_ItemsHolder") as Panel;
        UpdateSelectedItem();
    }

    /// <summary>
    /// when the items change we remove any generated panel children and add any new ones as necessary
    /// </summary>
    /// <param name="e"></param>
    protected override void OnItemsChanged(NotifyCollectionChangedEventArgs e)
    {
        base.OnItemsChanged(e);

        if (_itemsHolder == null)
        {
            return;
        }

        switch (e.Action)
        {
            case NotifyCollectionChangedAction.Reset:
                _itemsHolder.Children.Clear();

                if (base.Items.Count > 0)
                {
                    base.SelectedItem = base.Items[0];
                    UpdateSelectedItem();
                }

                break;

            case NotifyCollectionChangedAction.Add:
            case NotifyCollectionChangedAction.Remove:

                // Search for recently deleted items caused by a Drag/Drop operation
                if (e.NewItems != null && _deletedObject != null)
                {
                    foreach (var item in e.NewItems)
                    {
                        if (_deletedObject == item)
                        {
                            // If the new item is the same as the recently deleted one (i.e. a drag/drop event)
                            // then cancel the deletion and reuse the ContentPresenter so it doesn't have to be 
                            // redrawn. We do need to link the presenter to the new item though (using the Tag)
                            ContentPresenter cp = FindChildContentPresenter(_deletedObject);
                            if (cp != null)
                            {
                                int index = _itemsHolder.Children.IndexOf(cp);

                                (_itemsHolder.Children[index] as ContentPresenter).Tag =
                                    (item is TabItem) ? item : (this.ItemContainerGenerator.ContainerFromItem(item));
                            }
                            _deletedObject = null;
                        }
                    }
                }

                if (e.OldItems != null)
                {
                    foreach (var item in e.OldItems)
                    {

                        _deletedObject = item;

                        // We want to run this at a slightly later priority in case this
                        // is a drag/drop operation so that we can reuse the template
                        this.Dispatcher.BeginInvoke(DispatcherPriority.DataBind,
                            new Action(delegate ()
                            {
                                if (_deletedObject != null)
                                {
                                    ContentPresenter cp = FindChildContentPresenter(_deletedObject);
                                    if (cp != null)
                                    {
                                        this._itemsHolder.Children.Remove(cp);
                                    }
                                }
                            }
                        ));
                    }
                }

                UpdateSelectedItem();
                break;

            case NotifyCollectionChangedAction.Replace:
                throw new NotImplementedException("Replace not implemented yet");
        }
    }

    /// <summary>
    /// update the visible child in the ItemsHolder
    /// </summary>
    /// <param name="e"></param>
    protected override void OnSelectionChanged(SelectionChangedEventArgs e)
    {
        base.OnSelectionChanged(e);
        UpdateSelectedItem();
    }

    /// <summary>
    /// generate a ContentPresenter for the selected item
    /// </summary>
    void UpdateSelectedItem()
    {
        if (_itemsHolder == null)
        {
            return;
        }

        // generate a ContentPresenter if necessary
        TabItem item = GetSelectedTabItem();
        if (item != null)
        {
            CreateChildContentPresenter(item);
        }

        // show the right child
        foreach (ContentPresenter child in _itemsHolder.Children)
        {
            child.Visibility = ((child.Tag as TabItem).IsSelected) ? Visibility.Visible : Visibility.Collapsed;
        }
    }

    /// <summary>
    /// create the child ContentPresenter for the given item (could be data or a TabItem)
    /// </summary>
    /// <param name="item"></param>
    /// <returns></returns>
    ContentPresenter CreateChildContentPresenter(object item)
    {
        if (item == null)
        {
            return null;
        }

        ContentPresenter cp = FindChildContentPresenter(item);

        if (cp != null)
        {
            return cp;
        }

        // the actual child to be added.  cp.Tag is a reference to the TabItem
        cp = new ContentPresenter();
        cp.Content = (item is TabItem) ? (item as TabItem).Content : item;
        cp.ContentTemplate = this.SelectedContentTemplate;
        cp.ContentTemplateSelector = this.SelectedContentTemplateSelector;
        cp.ContentStringFormat = this.SelectedContentStringFormat;
        cp.Visibility = Visibility.Collapsed;
        cp.Tag = (item is TabItem) ? item : (this.ItemContainerGenerator.ContainerFromItem(item));
        _itemsHolder.Children.Add(cp);
        return cp;
    }

    /// <summary>
    /// Find the CP for the given object.  data could be a TabItem or a piece of data
    /// </summary>
    /// <param name="data"></param>
    /// <returns></returns>
    ContentPresenter FindChildContentPresenter(object data)
    {
        if (data is TabItem)
        {
            data = (data as TabItem).Content;
        }

        if (data == null)
        {
            return null;
        }

        if (_itemsHolder == null)
        {
            return null;
        }

        foreach (ContentPresenter cp in _itemsHolder.Children)
        {
            if (cp.Content == data)
            {
                return cp;
            }
        }

        return null;
    }

    /// <summary>
    /// copied from TabControl; wish it were protected in that class instead of private
    /// </summary>
    /// <returns></returns>
    protected TabItem GetSelectedTabItem()
    {
        object selectedItem = base.SelectedItem;
        if (selectedItem == null)
        {
            return null;
        }

        if (_deletedObject == selectedItem)
        {

        }

        TabItem item = selectedItem as TabItem;
        if (item == null)
        {
            item = base.ItemContainerGenerator.ContainerFromIndex(base.SelectedIndex) as TabItem;
        }
        return item;
    }
}

My main view:

Editor.xaml

<Window.Resources>
    <ResourceDictionary Source="Resources/GlobalDictionary.xaml" />
</Window.Resources>
<DockPanel>
    <DockPanel DockPanel.Dock="Top" KeyboardNavigation.TabNavigation="None">
        <Menu Height="20" KeyboardNavigation.TabNavigation="Cycle">
            <MenuItem Header="{StaticResource MenuFile}">
                <MenuItem Header="{StaticResource MenuOpen}" />
                <MenuItem Header="{StaticResource MenuSave}" />
                <MenuItem Header="{StaticResource MenuExit}" Command="{Binding Path=CloseCommand}" />
            </MenuItem>
            <MenuItem Header="{StaticResource MenuEditors}">
                <MenuItem Header="{StaticResource MenuMonster}" Command="{Binding Path=OpenMonsterEditor}"/>
            </MenuItem>
            <MenuItem Header="{StaticResource MenuHelp}">
                <MenuItem Header="{StaticResource MenuAbout}" />
                <MenuItem Header="{StaticResource MenuContact}" />
            </MenuItem>
        </Menu>
    </DockPanel>
    <controls:TabControlEx ItemsSource="{Binding AvailableEditors}"
                SelectedItem="{Binding CurrentEditor}"
                Style="{StaticResource BlankTabControlTemplate}">
    </controls:TabControlEx>
</DockPanel>

GlobalDictionary.xaml

<DataTemplate DataType="{x:Type vm:GBARomViewModel}">
    <vw:GBARomView />
</DataTemplate>

<DataTemplate DataType="{x:Type vm:MonsterViewModel}">
    <vw:MonsterView />
</DataTemplate>

<Style x:Key="BlankTabControlTemplate" TargetType="{x:Type control:TabControlEx}">
    <Setter Property="SnapsToDevicePixels" Value="true"/>
    <Setter Property="Template">
        <Setter.Value>
            <ControlTemplate TargetType="{x:Type control:TabControlEx}">
                <DockPanel>
                    <!-- This is needed to draw TabControls with Bound items -->
                    <StackPanel IsItemsHost="True" Height="0" Width="0" />
                    <Grid x:Name="PART_ItemsHolder" />
                </DockPanel>
            </ControlTemplate>
        </Setter.Value>
    </Setter>
</Style>

I'm not sure if the following is right, but each view extent TabItem as follow. The first view on startup is shown correctly.

GBARomView.xaml

<TabItem x:Class="FF6AE.View.GBARomView"
      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" 
      xmlns:local="clr-namespace:FF6AE.View"
      mc:Ignorable="d" 
      d:DesignHeight="380" d:DesignWidth="646"
      Background="BlueViolet">

    <Grid >

    </Grid>
</TabItem>

GBARomView.xaml.cs

public partial class GBARomView : TabItem
{
    public GBARomView()
    {
        InitializeComponent();
    }
}

Finally my main viewModel:

EDitorViewModel.cs

public class EditorViewModel: ViewModelBase
{
    ViewModelBase _currentEditor;
    ObservableCollection<ViewModelBase> _availableEditors;
    RelayCommand _closeCommand;
    RelayCommand _OpenMonsEditorCommand;

    public EditorViewModel()
    {
        base.DisplayName = (string)AppInst.GetResource("EditorName");

        _availableEditors = new ObservableCollection<ViewModelBase>();
        _availableEditors.Add(new GBARomViewModel());
        _availableEditors.Add(new MonsterViewModel());
        _availableEditors.CollectionChanged += this.OnAvailableEditorsChanged;

        _currentEditor = _availableEditors[0];
        _currentEditor.PropertyChanged += this.OnSubEditorChanged;
    }

    public ViewModelBase CurrentEditor
    {
        get
        {
            if (_currentEditor == null)
            {
                _currentEditor = new GBARomViewModel();
                _currentEditor.PropertyChanged += this.OnSubEditorChanged;
            }

            return _currentEditor;
        }
        set
        {
            _currentEditor = value;
             // this is the thing I was missing
             OnPropertyChanged("CurrentEditor");
        }
    }

    void OnSubEditorChanged(object sender, PropertyChangedEventArgs e)
    {
        // For future use
    }

    public ObservableCollection<ViewModelBase> AvailableEditors
    {
        get
        {
            return _availableEditors;
        }
    }

    void OnAvailableEditorsChanged(object sender, NotifyCollectionChangedEventArgs e)
    {
        // For future use
    }

    public ICommand OpenMonsterEditor
    {
        get
        {
            if (_OpenMonsEditorCommand == null)
                _OpenMonsEditorCommand = new RelayCommand(param => this.OpenRequestOpen());

            return _OpenMonsEditorCommand;
        }
    }
    void OpenRequestOpen()
    {
        _currentEditor = _availableEditors[1];
    }

    public ICommand CloseCommand
    {
        get
        {
            if (_closeCommand == null)
                _closeCommand = new RelayCommand(param => this.OnRequestClose());

            return _closeCommand;
        }
    }

    public event EventHandler RequestClose;

    void OnRequestClose()
    {
        EventHandler handler = this.RequestClose;
        if (handler != null)
            handler(this, EventArgs.Empty);
    }
}

So basically I am lost why clicking on the monster editor in the menu does not switch the view though currentEditor value is changed (fixed) and why on mouse over on the tabItem I lose the background testing color of the view (it turns to white). <- still not fixed!

Any help would be appreciated. Thanks in advance.

Edit: I was missing the OnPropertyChanged("CurrentEditor"); in the setter of CurrentEditor.

Community
  • 1
  • 1
madsiur
  • 85
  • 8

1 Answers1

0

Well basically my two questions were misunderstandings. First, the CurrentEditor attribute need OnPropertyChanged called in EditorViewModel.cs such as follow. This calls the ViewModelBase class OnPropertyChanged method:

public ViewModelBase CurrentEditor
{
    get
    {
        if (_currentEditor == null)
        {
            _currentEditor = new GBARomViewModel();
            _currentEditor.PropertyChanged += this.OnSubEditorChanged;
        }

        return _currentEditor;
    }
    set
    {
        _currentEditor = value;
        OnPropertyChanged("CurrentEditor");
    }
}

My second problem took longer to solve. I did not needed to have my views extent TabItem but instead something like DockPanel. This ensure the whole window will not have the TabItem IsMouseOver behavior of reverting the color back to default. Setting an background image such as follow work and I can navigate the view. I wasn't able to solve this with a control template of TabItem.

GbaRomView.xaml.cs

public partial class GBARomView : DockPanel
{
    public GBARomView()
    {
        InitializeComponent();
    }
}

GbaRomView.xaml

<DockPanel x:Class="FF6AE.View.GBARomView"
      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" 
      d:DesignHeight="380" d:DesignWidth="646">
    <DockPanel.Background>
        <ImageBrush ImageSource="/Resources/Images/DefaultBackground.png"></ImageBrush>
    </DockPanel.Background>
</DockPanel>
madsiur
  • 85
  • 8