Search code examples
c#wpfxamltabcontrol

Implement a TabControl with dynamic TabItem with the same layout


enter image description here

I will try to describe what I want to achive trough the interface of my program:

It's a Client that connects to a Server to retrieve some information about the running applications on a particular enviroment where this Server is running. By now it works only with 1 server, so if it is connected it needs to disconnect and reconnect again to another one to gain informations about another pc where another server is running. I want to allow to create more instances of my Client to connect to other Servers to gain informations about more pcs at the same time.

My design concept is to extends the interface that I have designed in the MainWindow through a TabControl. By the "+" or whatever button I want to create another instace of the Client interface but I need to maintain only the elements that are highlighted in red, while the element highleted in green must be in common to all the instances.

I now have a window where I put all the controls to show the processes and some other things, again all the tabs in the TabControl must have the same layout with different content, but they must share the same LogWindow (green box).

The TabControl showed in the image is a simple copy-paste from another image, I have not implemented it yet.

I have seen some tutorial on dynamic Tab Control Items, in particular this one.

The problem is that I don't properly understand how to port my layout of MainWindow as a standard template for other tabs.

Here's the code that I developed:

MainWindow.xaml

<Window x:Class="Project_Client.MainWindow"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
    xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
    xmlns:local="clr-namespace:Project_Client"
    xmlns:helper="clr-namespace:Helper"
    mc:Ignorable="d"
    ResizeMode="CanResizeWithGrip"
    MinHeight="480" MinWidth="640"
    Title="MainWindow" Height="480" Width="640" Closing="Window_Closing" 
    >
<Window.Resources>
    <BitmapImage x:Key="RevealImage" UriSource="./reveal_icon.png"></BitmapImage>
    <BitmapImage x:Key="HideImage" UriSource="./hide_icon.png"></BitmapImage>
</Window.Resources>

<Grid>
    
    <Grid>
        <Grid.ColumnDefinitions>
            <ColumnDefinition Width="*"></ColumnDefinition>
            <ColumnDefinition Width="auto"></ColumnDefinition>
        </Grid.ColumnDefinitions>
        <TextBlock FontWeight="Bold" VerticalAlignment="Top" Width="86" Height="16" Margin="13,76,0,0" HorizontalAlignment="Left">Process List:</TextBlock>

        <Grid Grid.Column="0" Name="mLeftGrid" Margin="0,0,0,183.4" HorizontalAlignment="Stretch">

            <Grid.Resources>
                <DataTemplate x:Key="StrProperty">
                    <TextBlock Text="{Binding Path=Str}"/>
                </DataTemplate>
                <DataTemplate x:Key="ImgProperty">
                    <Image Source="{Binding Path=Image}"  />
                </DataTemplate>
            </Grid.Resources>
            <Grid.RowDefinitions>
                <RowDefinition Height="228*"/>
                <RowDefinition Height="343*"/>
            </Grid.RowDefinitions>
            <Border Grid.RowSpan="2"
        BorderBrush="Cyan"
        BorderThickness="3"
        HorizontalAlignment="Stretch"
        VerticalAlignment="Stretch"/>
            <ProgressBar x:Name="progressBarProcess" Margin="13,66,-3,0" Height="6" VerticalAlignment="Top" HorizontalAlignment="Left" Width="316"/>
            <TextBlock x:Name="progressText" HorizontalAlignment="Stretch" TextWrapping="Wrap" Text="TextBlock" Width="auto" Margin="12,45,-12,0" Height="19" VerticalAlignment="Top"/>

            <Grid Name="mLeftInnerGrid" Margin="9.8,102,10,0" VerticalAlignment="Top" Grid.RowSpan="2" HorizontalAlignment="Stretch" SizeChanged="mLeftInnerGrid_SizeChanged">

                <ListView x:Name="mCurrentApps" HorizontalAlignment="Left" Height="auto" VerticalAlignment="Top" ItemsSource="{Binding Items, Mode=OneWay}" VirtualizingPanel.IsVirtualizing="true" 
     VirtualizingPanel.VirtualizationMode="Recycling" Margin="0,0,-155.2,0" SizeChanged="mCurrentApps_SizeChanged">
                    <ListView.ItemContainerStyle>
                        <Style TargetType="{x:Type ListViewItem}">
                            <Setter Property="Focusable" Value="False"/>
                            <Style.Triggers>
                                <Trigger Property="IsSelected" Value="True" >
                                    <Setter Property="FontWeight" Value="Bold" />
                                    <Setter Property="Foreground" Value="OrangeRed" />
                                </Trigger>
                            </Style.Triggers>
                            <Style.Resources>
                                <SolidColorBrush x:Key="{x:Static SystemColors.HighlightBrushKey}" Color="Transparent"/>
                            </Style.Resources>
                        </Style>
                    </ListView.ItemContainerStyle>
                    <ListView.View>
                        <GridView>
                            <GridViewColumn>
                                <GridViewColumn.Header>
                                    <GridViewColumnHeader Tag="App" Click="GridViewColumnHeader_Click_" Width="auto">App</GridViewColumnHeader>
                                </GridViewColumn.Header>
                                <GridViewColumn.CellTemplate>
                                    <DataTemplate>
                                        <StackPanel Orientation="Horizontal" MinWidth="64">
                                            <Image Width="16" Height="16" Source="{Binding Image}" Margin="4,0,16,0" />
                                            <TextBlock Text="{Binding App}" />
                                        </StackPanel>
                                    </DataTemplate>
                                </GridViewColumn.CellTemplate>
                            </GridViewColumn>
                            <GridViewColumn DisplayMemberBinding="{Binding Process}" >
                                <GridViewColumn.Header>
                                    <GridViewColumnHeader Tag="Process" Click="GridViewColumnHeader_Click_" Width="auto">Process</GridViewColumnHeader>
                                </GridViewColumn.Header>
                            </GridViewColumn>
                            <GridViewColumn FrameworkElement.FlowDirection="RightToLeft">
                                <GridViewColumn.CellTemplate>
                                    <DataTemplate>
                                        <TextBlock Text="{Binding ExecutionTimer}" HorizontalAlignment="Right"></TextBlock>
                                    </DataTemplate>
                                </GridViewColumn.CellTemplate>
                                <GridViewColumn.Header >
                                    <GridViewColumnHeader Tag="Timer" Click="GridViewColumnHeader_Click_" HorizontalContentAlignment="Right" >Active Time</GridViewColumnHeader>
                                </GridViewColumn.Header>
                            </GridViewColumn>
                        </GridView>
                    </ListView.View>

                </ListView>

            </Grid>

        </Grid>

        <Grid Grid.Column="1" Name="mRigthGrid" Margin="0,0,-0.4,182.4" HorizontalAlignment="Right">
            <Grid.RenderTransform>
                <TranslateTransform x:Name="mRightGridAnim"></TranslateTransform>
            </Grid.RenderTransform>
            <Border
        BorderBrush="Coral"
        BorderThickness="3"
        HorizontalAlignment="Stretch"
        VerticalAlignment="Stretch"/>
            <TextBlock FontWeight="Bold" Margin="0,76,248,0" HorizontalAlignment="Right" VerticalAlignment="Top" Width="auto" Height="16">Status:</TextBlock>
            <TextBlock x:Name="mAppStatus" Visibility="Visible" Width="auto" Margin="0,76,45,0" Height="16" VerticalAlignment="Top" HorizontalAlignment="Right"/>
            <TextBox x:Name="mKeyPressed" HorizontalAlignment="Right" Margin="0,98,28,149" TextWrapping="Wrap" Text="..." Width="247" Loaded="keyPressed_Loaded" FontSize="20" IsReadOnly="True"/>
            <Button x:Name="mRecordingButton" Content="Start recording keys" Margin="0,0,77,98" Click="button_Click" HorizontalAlignment="Right" Width="139" Height="19" VerticalAlignment="Bottom"/>
            <TextBlock x:Name="PcName" Margin="8,45,229,0" TextWrapping="Wrap" Text="PC-??????" HorizontalAlignment="Right" Width="57" Height="15" VerticalAlignment="Top" />
            <TextBlock x:Name="numberOfProcesses" HorizontalAlignment="Right" Margin="0,45,132,0" TextWrapping="Wrap" Text="Processes: ???" Height="16" VerticalAlignment="Top"/>

                    </Grid>
        
    </Grid>

    <DockPanel x:Name="mRevealButton" PreviewMouseLeftButtonUp="mRevealButton_PreviewMouseLeftButtonUp" Margin="0,97,-0.4,183.4" HorizontalAlignment="Right" Width="10">
        <DockPanel.Style>
            <Style TargetType="{x:Type DockPanel}">
                <Style.Triggers>
                    <Trigger Property="IsMouseOver" Value="True">
                        <Setter Property="Background" Value="LightGray"/>
                        <Setter Property="ToolTip" Value="Click to show/hide keys input box"/>
                    </Trigger>

                </Style.Triggers>
            </Style>
        </DockPanel.Style>
        <Image x:Name="mRevealImage" Width="16" Source="reveal_icon.png" HorizontalAlignment="Center" VerticalAlignment="Center" />
    </DockPanel>
    <Rectangle x:Name="Background" Visibility="Hidden">
        <Rectangle.Fill>
            <SolidColorBrush Color="{DynamicResource {x:Static SystemColors.InactiveCaptionColorKey}}"/>
        </Rectangle.Fill>
    </Rectangle>
    <ListBox x:Name="logWindow" ItemsSource="{Binding LogEntry, Mode=OneWay}" Grid.IsSharedSizeScope="True" VirtualizingPanel.IsVirtualizing="true" 
     VirtualizingPanel.VirtualizationMode="Recycling" Margin="10,0,8.6,41.4" Height="134" VerticalAlignment="Bottom" >
        <ListBox.ItemContainerStyle>
            <Style TargetType="{x:Type ListBoxItem}" >
                <Setter Property="FontStyle" Value="Italic"></Setter>
                <Style.Triggers>
                    <DataTrigger Binding="{Binding Path=IsError, UpdateSourceTrigger=PropertyChanged}" Value="true">
                        <Setter Property="Foreground" Value="Red"></Setter>

                    </DataTrigger>
                    <DataTrigger Binding="{Binding Path=IsWarning, UpdateSourceTrigger=PropertyChanged}" Value="true">
                    </DataTrigger>

                </Style.Triggers>
                <Style.Resources>
                    <SolidColorBrush x:Key="{x:Static SystemColors.HighlightBrushKey}" Color="Transparent"/>

                </Style.Resources>
            </Style>
        </ListBox.ItemContainerStyle>
        <ListBox.ItemTemplate>
            <DataTemplate>
                <Grid>
                    <Grid.ColumnDefinitions>
                        <ColumnDefinition Width="auto" SharedSizeGroup="Icon" />
                        <ColumnDefinition Width="auto" MinWidth="64" SharedSizeGroup="Log" />
                    </Grid.ColumnDefinitions>
                    <Image Grid.Column="0"  Source="{Binding Path = Img}" Width="16" Height="16"/>
                    <TextBlock Grid.Column="1" Text="{Binding Path = log}" Margin="4,0,0,0" TextTrimming="CharacterEllipsis" HorizontalAlignment="Stretch" />
                </Grid>
            </DataTemplate>
        </ListBox.ItemTemplate>
    </ListBox>
    <Image x:Name="noConnectionImage" Source="no_connection.png" Width="256" Height="256" HorizontalAlignment="Center" VerticalAlignment="Center" Opacity="0.6" Visibility="Hidden" Margin="194,19,183.6,175.4" />
    <TextBlock x:Name="mBlockServerName" VerticalAlignment="Bottom" HorizontalAlignment="Left" Margin="10,0,0,10.4" TextWrapping="Wrap" Text="Server:" Grid.Row="1" />
    <TextBox x:Name="mBoxServerName" VerticalAlignment="Bottom" HorizontalAlignment="Left" Margin="69,0,0,10.4" TextWrapping="Wrap" Text="" Width="123" Grid.Row="1"  />
    <TextBlock x:Name="mBlockServerPort" VerticalAlignment="Bottom" HorizontalAlignment="Left" Margin="210,0,0,10.4" TextWrapping="Wrap" Text="Port:" Grid.Row="1"/>
    <TextBox x:Name="mBoxServerPort" VerticalAlignment="Bottom" HorizontalAlignment="Left" Margin="251,0,0,10.4" TextWrapping="Wrap" Text="" Width="47" Grid.Row="1" />
    <Button x:Name="mButtonConnect" VerticalAlignment="Bottom" HorizontalAlignment="Right" Content="Connect"  Margin="0,0,104.6,10.4" Width="76" Click="mButtonConnect_Click" Grid.Row="1"/>
    <Button x:Name="mButtonDisconnect" VerticalAlignment="Bottom" HorizontalAlignment="Right" Content="Disconnect"  Margin="0,0,9.6,10.4"  Width="75" Grid.Row="1" Click="mButtonDisconnect_Click"/>
</Grid>

MainWindow.cs (Only methods that are useful to populate the views)

public ObservableCollection<ProcessData> Items { get; set; }
public ObservableCollection<LogItem> LogEntry { get; set; }
public static MainWindow myInstance;
private LogItem log;
private Client client;
ProcessData indexFocused;
List<ProcessData> lop = new List<ProcessData>();
public String serverIp;
public long port;


public MainWindow()
{
    InitializeComponent();  
    myInstance = this;
    InitializeUI();
    attachLogSource();
    this.DataContext = this;
}

public void InitializeUI()
{
    mButtonConnect.IsEnabled = true;
    mAppStatus.Text = "Disconnected.";
    noConnectionImage.Visibility = Visibility.Visible;
    Background.Visibility = Visibility.Visible;
    //mCurrentApps.Visibility = Visibility.Hidden;
    //Items = new ObservableCollection<ProcessData>();
    noConnectionImage.Visibility = Visibility.Visible;
    Background.Visibility = Visibility.Visible;
    noConnectionImage.Source = new BitmapImage(new Uri(thisPath +  "no_connection.png"));
    progressBarProcess.Visibility = Visibility.Hidden;
    progressText.Visibility = Visibility.Hidden;
    //PcName.Visibility = Visibility.Hidden;
   // numberOfProcesses.Visibility = Visibility.Hidden;
    mBoxServerName.Text = "127.0.0.1";
    mBoxServerPort.Text = "11000";
    mButtonDisconnect.IsEnabled = false;
    //mCurrentApps.Visibility = Visibility.Hidden;
    mRecordingButton.IsEnabled = true;
    mRevealButton.Visibility = Visibility.Visible;
    mRevealImage.Source = new BitmapImage(new Uri(thisPath + "hide_icon.png"));
    GridViewColumnResizeBehaviour b = new GridViewColumnResizeBehaviour();
    b.Attach(mCurrentApps);
}

private void mButtonConnect_Click(object sender, RoutedEventArgs e)
{
    client = new Client();
    attachListSource();
    
    if (!(mBoxServerName.Text.Trim() == String.Empty && mBoxServerPort.Text.Trim() == String.Empty))
    {
        Items.Clear();
        log = new LogItem();
        log.log = "Connecting...";
        LogMessage(log);
        //mCurrentApps.Visibility = Visibility.Hidden;
        mRecordingButton.IsEnabled = false;
        client.wr_monitor();
    }
    else
    {
        log.log = "Must insert a valid address and port.";
        LogMessage(log);
    }
        //MessageBox.Show("Must insert a valid server address and port", "Error", MessageBoxButton.OK, MessageBoxImage.Exclamation);
}

private void attachListSource()
{
    Items = new ObservableCollection<ProcessData>();
    mCurrentApps.ItemsSource = Items;
    Items.CollectionChanged += new System.Collections.Specialized.NotifyCollectionChangedEventHandler(takeFocus);
}

private void attachLogSource()
{
    if(LogEntry == null)
    {
        LogEntry = new ObservableCollection<LogItem>();
        logWindow.ItemsSource = LogEntry;
        LogEntry.CollectionChanged += (s, e) =>
        {
            if (e.Action ==
                System.Collections.Specialized.NotifyCollectionChangedAction.Add)
            {
                logWindow.ScrollIntoView(logWindow.Items[logWindow.Items.Count - 1]);
            }
        };
    }
}

private void updateList()
{
    var l = lop.OrderBy(o => o.App);
    this.Dispatcher.Invoke(new Action(() =>
    {
        mCurrentApps.Visibility = Visibility.Visible;
        mCurrentApps.ItemsSource = l;
        progressText.Text = "Complete! (" + lop.Count + ")/(" + lop.Count + ")";
        numberOfProcesses.Text = "Processes: " + Items.Count.ToString();
        log = new LogItem();
        log.log = "Received all the processes (" + lop.Count + ")";
        LogMessage(log);
        mCurrentApps.Items.Refresh();
    }));
}

private void mButtonDisconnect_Click(object sender, RoutedEventArgs e)
{
    Client.EndConnect();
    mButtonConnect.IsEnabled = true;
}

private void LogMessage(LogItem l)
{
    l.Img = SystemIcons.Information.ToImageSource() as BitmapSource;
    Application.Current.Dispatcher.Invoke(() =>
    {
        LogEntry.Add(l);
    });
}

private void GridViewColumnHeader_Click_(object sender, RoutedEventArgs e)
{
    ListViewColumnOptions.GridViewColumnHeader_Click_(sender, e);
}
private void mRevealButton_PreviewMouseLeftButtonUp(object sender, MouseButtonEventArgs e)
{
    DockPanelOptions.PerformClickAction(sender, e);
}

private void mCurrentApps_SizeChanged(object sender, SizeChangedEventArgs e)
{
    ListViewColumnOptions.ListView_SizeChanged(sender, e);
}

private void mLeftInnerGrid_SizeChanged(object sender, SizeChangedEventArgs e)
{
        mCurrentApps.Width = mLeftInnerGrid.ActualWidth;   
}

In particular I want to share between tabs only the listbox section (logWindow) that is general between the views, but I want to populate each tabs with different data.

From what I understand I need to create a userControl and implement it as a Data Template inside every tab that will be created but I have not the knowledge to implement it without referring to a pre-built example.


Solution

  • You can do it by simple data binding (an idea only since the question is somehow vague):

    <Window x:Class="FileHelperTest.Window1"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        Title="Window1" Height="300" Width="300">
    <Grid>
        <Grid.RowDefinitions>
            <RowDefinition Height="2*"/>
            <RowDefinition Height="*"/>
            <RowDefinition Height="Auto"/>
        </Grid.RowDefinitions>
        <TabControl ItemsSource="{Binding AllServers}" SelectedItem="{Binding SelectedServer, Mode=TwoWay}">
            <TabControl.ItemTemplate>
                <!-- this is the header template-->
                <DataTemplate>
                    <Grid>
                    <TextBlock Text="{Binding Name}"/>
                    <Button HorizontalAlignment="Stretch" VerticalAlignment="Stretch"
                        Content="{Binding Name}"
                        Command="{Binding DataContext.AddNew, RelativeSource={RelativeSource AncestorType=TabControl}, Mode=OneTime}" 
                        CommandParameter="{Binding}" >
                        <Button.Style>
                            <Style TargetType="{x:Type  Button}">
                                <Setter Property="Visibility" Value="Collapsed"/>
                                <Style.Triggers>
                                    <DataTrigger Binding="{Binding Name}" Value="+" >
                                        <Setter Property="Visibility" Value="Visible"/>
                                    </DataTrigger>
                                </Style.Triggers>
                            </Style>
                        </Button.Style>
                    </Button>
                    </Grid>
                </DataTemplate>
            </TabControl.ItemTemplate>
            <TabControl.ContentTemplate>
                <!-- this is the body of the TabItem template-->
                <DataTemplate>
                    <TextBlock Text="{Binding SomeServerPropertyToDisplay}" />
                </DataTemplate>
            </TabControl.ContentTemplate>
        </TabControl>
        <DataGrid ItemsSource="{Binding LogMessages}" Grid.Row="1"/>
        <StackPanel Orientation="Horizontal" Grid.Row="2">
            <TextBlock Text="{Binding Path=SelectedServer.Name, StringFormat=Current server: {0}}"/>
            <Button Command="{Binding Connect, Mode=OneTime}" Content="Connect"/>
            <Button Command="{Binding Disconnect, Mode=OneTime}" Content="Disconnect"/>
        </StackPanel>
    </Grid>
    

    (The below code uses nuget package Fody.PropertyChanged and a standard implementation of ICommand)

    [ImplementPropertyChanged]
    public partial class Window1 : Window
    {
        MyViewModel VM;
        public Window1()
        {
            InitializeComponent();
            this.VM = new MyViewModel();
            this.DataContext = this.VM;
        }
    }
    
    [ImplementPropertyChanged]
    public class MyViewModel
    {
        public ObservableCollection<LogMessage> LogMessages { get; set; }
        public ObservableCollection<ServerTab> AllServers { get; set; }
        public ServerTab SelectedServer { get; set; }
    
        public MyViewModel()
        {
            this.AllServers = new ObservableCollection<ServerTab>();
            this.AllServers.Add(new ServerTab { Name = "+" });
    
            this.LogMessages = new ObservableCollection<LogMessage>();
        }
    
        private ICommand _AddNew;
        public ICommand AddNew { get { return _AddNew ?? (_AddNew = new DelegateCommand(a => AddNewCommand(a))); }}
        private void AddNewCommand(object item)
        {
            var senderItem = (ServerTab)item;
            if (senderItem.Name == "+")
            {
                var newItem = new ServerTab { Name = "Name " + (this.AllServers.Count) };
                this.AllServers.Remove(senderItem);
                this.AllServers.Add(newItem);
                this.AllServers.Add(senderItem);
                this.LogMessages.Add(new LogMessage { Time = DateTime.Now, Message = newItem.Name + " added" });
            }
        }
    
        private ICommand _Connect;
        public ICommand Connect{ get { return _Connect ?? (_Connect = new DelegateCommand(a => ConnectCommand(a))); }}
        private void ConnectCommand(object item)
        {
            this.LogMessages.Add(new LogMessage { Time = DateTime.Now, Message = this.SelectedServer.Name+" connected" });
        }
    
        private ICommand _Disconnect;
        public ICommand Disconnect{ get { return _Disconnect ?? (_Disconnect = new DelegateCommand(a => DisconnectCommand(a))); }}
        private void DisconnectCommand(object item)
        {
            this.LogMessages.Add(new LogMessage { Time = DateTime.Now, Message = this.SelectedServer.Name +" disconnected" });
        }
    }
    
    [ImplementPropertyChanged]
    public class ServerTab
    {
        public string Name { get; set; }
        public string SomeServerPropertyToDisplay { get; set; }
        public ServerTab()
        {
            SomeServerPropertyToDisplay = DateTime.Now.Ticks.ToString();
        }
    }
    
    [ImplementPropertyChanged]
    public class LogMessage
    {
        public DateTime Time { get; set; }
        public string Message { get; set; }
    }
    

    EDIT: I updated the code with the basic implementation of MVVM