There is a simpler version using
Data Binding[
^] and will simplfy your code.
Typically,
DataBinding
is used with the
MVVM pattern but can be used with code-behind. Below is a hybrid code solution -
not my ideal choice.
What I am suggesting is to use templates - one for the
Add Button
and one for the
TabItem
. We use a
TemplateSelector
to identify which template is used. One is required for the Header, another for the Content Area.
When working with
Data Binding
, property changes need to be broadcasted using the
INotifyPropertyChanged Interface[
^] so that binding is aware of any changes.
Here I have a typical base class that implements this interface:
public abstract class ObservableObject : INotifyPropertyChanged
{
public void Set<TValue>(ref TValue field,
TValue newValue,
[CallerMemberName] string propertyName = "")
{
if (!EqualityComparer<TValue>.Default.Equals(field, default)
&& field!.Equals(newValue))
return;
field = newValue;
PropertyChanged?.Invoke(this,
new PropertyChangedEventArgs(propertyName));
}
public event PropertyChangedEventHandler? PropertyChanged;
}
We require the
ObservableObject
class to notify the UI of the
ProgressBar.Value
updates. As each TabItem has its own ProgressBar control + Header + Content, I have used a class for each TabItem to encapsulate functionality:
public class TabItemViewModel : ObservableObject
{
public TabItemViewModel(string headerText, int maxCount)
{
this.headerText = headerText;
MaxCount = maxCount;
percentageComplete = 0;
}
private string headerText;
private int percentageComplete;
public string HeaderText
{
get => headerText;
set => Set(ref headerText, value);
}
public int MaxCount { get; }
public int PercentageComplete
{
get => percentageComplete;
set => Set(ref percentageComplete, value);
}
public async Task DoWork()
{
for (int i = 0; i <= MaxCount; i++)
{
await Task.Delay(100);
PercentageComplete = (i * 100) / MaxCount;
}
}
}
If you look at the
PercentageComplete
property, you can see that I set the value using the wrapper method for the
INotifyPropertyChanged.PropertyChanged
event. IF a property value never changes once the binding is set (ie: when the class is created), then you do not need to do this.
I have made the Action method
DoWork()
asynchronous. This means that the workload will happen on a seperate thread to the UI. Also, as we have encapsulated the workload + data, it then becomes self-tracking via the data binding.
Next we set up the code-behind to: 1. Set up the collection holding the TabItems; 2. Add new TabItems to the collection. This is a
Data First approach. The UI then becomes the View of the Data.
public partial class MainWindow : Window
{
public MainWindow()
{
InitializeComponent();
}
ObservableCollection<TabItemViewModel> tabItems;
public ObservableCollection<TabItemViewModel> TabItems
{
get
{
if (tabItems is null)
{
tabItems = new ObservableCollection<TabItemViewModel>();
IEditableCollectionView itemsView = (IEditableCollectionView)
CollectionViewSource.GetDefaultView(tabItems);
itemsView.NewItemPlaceholderPosition =
NewItemPlaceholderPosition.AtEnd;
}
return tabItems;
}
}
private void Button_Click(object sender, RoutedEventArgs e)
{
TabItemViewModel newTabItem = new($"Tab {tabItems.Count + 1}", 100);
TabItems.Add(newTabItem);
TabControl.SelectedItem = newTabItem;
_ = newTabItem.DoWork();
}
}
We are almost ready for the view. But before we do, we need to implement a simple
DataTemplateSelector[
^] - do we ue the
Add Button Template
or the
Tab Item Template
?
public class TemplateSelector : DataTemplateSelector
{
public DataTemplate ItemTemplate { get; set; }
public DataTemplate AddButtonTemplate { get; set; }
public override DataTemplate SelectTemplate
(object item, DependencyObject container)
{
return item == CollectionView.NewItemPlaceholder
? AddButtonTemplate
: ItemTemplate;
}
}
Last, the view with the TabControl. There are two parts - the Header & the Content. The Template will look at the TabItems Collection. When the collection has a new item added, the Collection will trigger a
CollectionChanged Event[
^] and the Data Binding will notify the TabControl and the UI will update automagically.
<Window x:Class="WpfTabsWithProgressBar.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:root="clr-namespace:WpfTabsWithProgressBar"
mc:Ignorable="d" Title="MainWindow" Height="450" Width="800"
x:Name="ThisWindow">
<Grid DataContext="{Binding ElementName=ThisWindow}">
<Grid.Resources>
<DataTemplate x:Key="AddTabButtonContentTemplate">
<Grid Height="20"/>
</DataTemplate>
<DataTemplate x:Key="AddTabButtonHeaderTemplate">
<Button Content="+" Click="Button_Click"/>
</DataTemplate>
<DataTemplate x:Key="TabItemContentTemplate">
<Grid>
<ProgressBar
Margin="0 10"
Height="20"
Minimum="0"
Maximum="{Binding MaxCount}"
Value="{Binding PercentageComplete}" />
</Grid>
</DataTemplate>
<DataTemplate x:Key="TabItemHeaderTemplate">
<TextBlock Text="{Binding HeaderText}"/>
</DataTemplate>
<root:TemplateSelector
x:Key="HeaderTemplateSelector"
AddButtonTemplate="{StaticResource
AddTabButtonHeaderTemplate}"
ItemTemplate="{StaticResource
TabItemHeaderTemplate}"/>
<root:TemplateSelector
x:Key="ContentTemplateSelector"
AddButtonTemplate="{StaticResource
AddTabButtonContentTemplate}"
ItemTemplate="{StaticResource
TabItemContentTemplate}"/>
</Grid.Resources>
<TabControl x:Name="TabControl" Margin="10"
ItemsSource="{Binding TabItems}"
ItemTemplateSelector="{StaticResource
HeaderTemplateSelector}"
ContentTemplateSelector="{StaticResource
ContentTemplateSelector}">
</TabControl>
</Grid>
</Window>
When you run the app, open 3+ tabs, each a few seconds apart. Each tab will track it's own progress. Switch back and forth between the added tabs to see them updating independently.
Hope this helps ... enjoy!