MahApps / MahApps.Metro

A framework that allows developers to cobble together a better UI for their own WPF applications with minimal effort.
https://mahapps.com
MIT License
9.29k stars 2.45k forks source link

MetroTabControl/MetroTabItem - make it more MVVM-friendly #551

Closed shiftkey closed 10 years ago

shiftkey commented 11 years ago

To recap: #543 and #548 were attempts to fix a few things. And this issue crept in as well.

So I just pushed this fix here to ensure it doesn't come back - you can see the tests here for yourself.

As the commit message suggests, it's not intended to be a permanent fix.

What I'd like to hear from you is:

Yes, the demo app could do with a better MVVM example. For the moment I'm tending towards introducing something like the Caliburn.Micro sample (or just updating that to do more than Flyouts).

Thoughts?

cc @bitterskittles @Amrykid

AzureKitsune commented 11 years ago

I have a little MVVM text editor I am writing on the side. I would bind the CloseTabCommand (TabControl-level, not TabItem level) to my ViewModel and handle closing from there. Before my changes ( #517 #520 #543 ), this would have taken a bit of code to do. Now, it is as simple as ever.

Oh, and the MVVM Framework I'm using is a homebrew multiplatform one I wrote (which needs a rewrite, eventually): Amrykid/Crystal

View

<metro:MetroWindow x:Name="metroWindow" x:Class="Lympha.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:metro="clr-namespace:MahApps.Metro.Controls;assembly=MahApps.Metro"
        xmlns:vm="clr-namespace:Lympha.ViewModel"
        xmlns:model="clr-namespace:Lympha.Model"
        xmlns:i="http://schemas.microsoft.com/expression/2010/interactivity"
        xmlns:Behaviours="clr-namespace:MahApps.Metro.Behaviours;assembly=MahApps.Metro"
        xmlns:i18n="clr-namespace:Crystal.Localization;assembly=Amrykid.Crystal"
        xmlns:toolkit="http://schemas.xceed.com/wpf/xaml/toolkit"
        Title="Lympha" Height="350" Width="525" Style="{DynamicResource CleanWindowStyleKey}">
    <i:Interaction.Behaviors>
        <Behaviours:BorderlessWindowBehavior ResizeWithGrip="True"/>
    </i:Interaction.Behaviors>
    <Window.DataContext>
        <vm:MainWindowViewModel />
    </Window.DataContext>
    <Window.Resources>
        <ResourceDictionary>
            <ResourceDictionary.MergedDictionaries>
                <ResourceDictionary Source="pack://application:,,,/MahApps.Metro;component/Styles/Colours.xaml" />
                <ResourceDictionary Source="pack://application:,,,/MahApps.Metro;component/Styles/Fonts.xaml" />
                <ResourceDictionary Source="pack://application:,,,/MahApps.Metro;component/Styles/Controls.xaml" />
                <ResourceDictionary Source="pack://application:,,,/MahApps.Metro;component/Styles/Controls.AnimatedSingleRowTabControl.xaml" />
                <ResourceDictionary Source="pack://application:,,,/Lympha;component/Resources/Icons.xaml" />
                <ResourceDictionary Source="pack://application:,,,/MahApps.Metro;component/Styles/Accents/Blue.xaml" />
                <ResourceDictionary Source="pack://application:,,,/MahApps.Metro;component/Styles/Accents/BaseLight.xaml" />
                <ResourceDictionary Source="pack://application:,,,/MahApps.Metro;component/Styles/Clean/CleanWindow.xaml"/>
            </ResourceDictionary.MergedDictionaries>
        </ResourceDictionary>
    </Window.Resources>
    <Grid Margin="0 -30 0 0">
        <Grid.RowDefinitions>
            <RowDefinition Height="25"/>
            <RowDefinition Height="30"/>
            <RowDefinition Height="100*"/>
            <RowDefinition Height="50"/>
        </Grid.RowDefinitions>

        <Menu x:Name="MainMenu" Grid.Row="0" Width="Auto">
            <MenuItem Header="{i18n:CrystalLocalizedValueMarkup Key='FileMenuItem', NullValue='File'}">
                <MenuItem Header="{i18n:CrystalLocalizedValueMarkup Key='NewMenuItem', NullValue='New'}" Command="{Binding NewCommand}"/>
            </MenuItem>
        </Menu>

        <ToolBar Grid.Row="1">
            <Button Style="{DynamicResource MetroCircleButtonStyle}" Width="30" Height="30" Command="{Binding NewCommand}" ClickMode="Press">
                <Rectangle Width="10" Height="10">
                    <Rectangle.Fill>
                        <VisualBrush Stretch="Fill"
                                                Visual="{StaticResource appbar_add}" />
                    </Rectangle.Fill>
                </Rectangle>
            </Button>
        </ToolBar>

        <metro:MetroTabControl Grid.Row="2" x:Name="EditorTabControl" ItemsSource="{Binding Tabs, UpdateSourceTrigger=PropertyChanged}" 
                                                VerticalAlignment="Stretch" HorizontalAlignment="Stretch" CloseTabCommand="{Binding CloseTabCommand}" 
                               SelectedItem="{Binding SelectedFile, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}" IsSynchronizedWithCurrentItem="True">
            <metro:MetroTabControl.Resources>
                <DataTemplate x:Key="contentTemplate" x:Shared="False">
                    <toolkit:RichTextBox Text="{Binding Text, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}" 
                                             VerticalAlignment="Stretch" HorizontalAlignment="Stretch" Margin="2" SpellCheck.IsEnabled="True"/>
                </DataTemplate>
            </metro:MetroTabControl.Resources>
            <metro:MetroTabControl.ItemContainerStyle>
                <Style TargetType="{x:Type metro:MetroTabItem}">
                    <Setter Property="Header" Value="{Binding Filename, UpdateSourceTrigger=PropertyChanged}"/>
                    <Setter Property="metro:MetroTabItem.CloseButtonEnabled" Value="True"/>
                    <Setter Property="ContentTemplate" Value="{StaticResource contentTemplate}"/>
                </Style>
            </metro:MetroTabControl.ItemContainerStyle>
        </metro:MetroTabControl>

        <Grid Grid.Row="3">
            <!-- Psuedo AppBar -->

            <StackPanel Orientation="Horizontal">
                <StackPanel Orientation="Horizontal" Width="300">
                    <Button Style="{DynamicResource MetroCircleButtonStyle}" Width="50" Height="50" ClickMode="Press">
                        <Rectangle Width="25" Height="25">
                            <Rectangle.Fill>
                                <VisualBrush Stretch="Fill"
                                                Visual="{StaticResource appbar_edit}" />
                            </Rectangle.Fill>
                        </Rectangle>
                    </Button>
                    <Button Style="{DynamicResource MetroCircleButtonStyle}" Width="50" Height="50" Command="{Binding NewCommand}" ClickMode="Press">
                        <Rectangle Width="25" Height="25">
                            <Rectangle.Fill>
                                <VisualBrush Stretch="Fill"
                                                Visual="{StaticResource appbar_add}" />
                            </Rectangle.Fill>
                        </Rectangle>
                    </Button>
                    <Button Style="{DynamicResource MetroCircleButtonStyle}" Width="50" Height="50" Command="{Binding NewCommand}" ClickMode="Press">
                        <Rectangle Width="25" Height="25">
                            <Rectangle.Fill>
                                <VisualBrush Stretch="Fill"
                                                Visual="{StaticResource appbar_add}" />
                            </Rectangle.Fill>
                        </Rectangle>
                    </Button>
                </StackPanel>
            </StackPanel>
        </Grid>
    </Grid>
</metro:MetroWindow>

ViewModel

public class MainWindowViewModel : BaseViewModel
    {
        public MainWindowViewModel()
        {
            Tabs = new ObservableCollection<Model.OpenedFile>();

            CloseTabCommand = CommandManager.CreateCommand(x =>
            {
                OpenedFile file = x as OpenedFile;

                Tabs.Remove(file);

                CloseTabCommand.SetCanExecute(Tabs.Count > 1);
            });

            NewCommand = CommandManager.CreateCommand(x =>
            {
                AddNewTab();
            });

            AddNewTab();
            Tabs[0].Text = "Testing";

        }

        private void AddNewTab()
        {
            var tab = new Model.OpenedFile();

            Tabs.Add(tab);
            SelectedFile = tab;

            CloseTabCommand.SetCanExecute(Tabs.Count > 1);
        }

        public ObservableCollection<Model.OpenedFile> Tabs
        {
            get { return GetPropertyOrDefaultType<ObservableCollection<Model.OpenedFile>>(x => this.Tabs); }
            set { SetProperty<ObservableCollection<Model.OpenedFile>>(x => this.Tabs, value); }
        }
        public OpenedFile SelectedFile
        {
            get { return GetPropertyOrDefaultType<OpenedFile>(x => this.SelectedFile); }
            set { SetProperty<OpenedFile>(x => this.SelectedFile, value); }
        }

        public CrystalCommand CloseTabCommand { get; set; }
        public CrystalCommand NewCommand { get; set; }

Model

public class OpenedFile: BaseModel
    {
        public OpenedFile()
        {
            Filename = "Untitled";
        }

        public string FullFilename
        {
            get { return (string)GetProperty(x => this.FullFilename); }
            set { SetProperty(x => this.FullFilename, value); }
        }

        public string Filename
        {
            get { return (string)GetProperty(x => this.Filename); }
            set { SetProperty(x => this.Filename, value); }
        }

        public string Text
        {
            get { return (string)GetProperty(x => this.Text); }
            set { SetProperty(x => this.Text, value); }
        }
    }
bitterskittles commented 11 years ago

I think I could live without a closable TabItem, and such control extensions don't really help a developer save time. Xaml already makes it super easy to restyle/template a control, and every developer has a different way of implementing things, whether it be codebehind or one of the flavors of MVVM.

Take https://wpftoolkit.codeplex.com/ for example: it has some extended controls such as: Calculator, ChildWindow, DoubleUpDown, SplitButton, PropertyGrid etc. It is tempting to use these controls at first, but it takes at least the same amount effort to restyle them to match their look to your app's theme, when you could simply use your already styled controls to implement the same functionality:

<TabControl ItemsSource="{Binding Tabs}">
  <TabControl.ItemTemplate>
    <DataTemplate>
      <StackPanel Orientation="Horizontal">
        <TextBlock Text="{Binding Header}" />
        <Button Command="{Binding CloseCommand}" Content="X" />
      </StackPanel>
    </DataTemplate>
  </TabControl.ItemTemplate>
</TabControl>

Writing the code above took less than a minute for example, and it is more maintainable because I intend to use it with data binding and I don't need to worry about other scenarios like codebedind or TabItems in XAML.

The same goes for these as well: https://github.com/MahApps/MahApps.Metro/issues/116 https://github.com/MahApps/MahApps.Metro/issues/509 https://github.com/MahApps/MahApps.Metro/pull/530

It is simpler and more maintainable to roll your own extended controls if your goal is not to please everyone.

These are just my thoughts, but MetroTabControl and the close button works just fine now, and it is much appreciated ^^

shiftkey commented 11 years ago

Thanks for the feedback guys.

I think the next step is to either:

Either way, I think some documentation about this would be awesome. It's on me to get that new site to a point where it can go-live and people can start contributing documentation as well.

I'll leave this open for other feedback about this in the meantime.

pcriv commented 11 years ago

so, i have a doubt, closable metro tab can be use yet?

AzureKitsune commented 11 years ago

@pablocrivella The closable metro tab works already. We're just modifying it and the MetroTabControls to be more MVVM friendly.

shiftkey commented 11 years ago

@pablocrivella 0.11.0.36-ALPHA has the fixes from the past couple of days. This is discussing how to "make things nicer"...

pcriv commented 11 years ago

is it possible to bind MetroTabControl ItemSource to a collection of a Custom Object, keeping the closable tab behavior?