Click here to Skip to main content
15,564,534 members
Please Sign up or sign in to vote.
0.00/5 (No votes)
I'm trying to create and start a new progressBar every click action on button start that exist in the new TabItem ( after clicking on the "+") , so the goal is to run multiple progressBar in the same time , these progressBar will be created on each new Tab Item , and will run when clicking on Start Button of ( each tab ) , so each tab item will have a Button and a progressBar .

How can I do this?


What I have tried:

MainWidnow.xaml.cs

private List<TabItem> _tabItems;
    private TabItem _tabAdd;
    ProgressBar pr;
    public MainWindow()
    {
        try
        {
           
            InitializeComponent();

            // initialize tabItem array
            _tabItems = new List<TabItem>();

            // add a tabItem with + in header 
            _tabAdd = new TabItem();
            _tabAdd.Header = "+";
            // tabAdd.MouseLeftButtonUp += new MouseButtonEventHandler(tabAdd_MouseLeftButtonUp);

            _tabItems.Add(_tabAdd);

            // add first tab
            this.AddTabItem();

            // bind tab control
            tabDynamic.DataContext = _tabItems;

            tabDynamic.SelectedIndex = 0;

      
        }
        catch (Exception ex)
        {
            MessageBox.Show(ex.Message);
        }
    }

    private TabItem AddTabItem()
    {
        int count = _tabItems.Count;
        pr = new ProgressBar();
        pr.Width = 150;
        pr.Height = 30;
        // create new tab item
        TabItem tab = new TabItem();

        tab.Header = string.Format("Tab {0}", count);
        tab.Name = string.Format("tab{0}", count);
        tab.HeaderTemplate = tabDynamic.FindResource("TabHeader") as DataTemplate;

        tab.MouseDoubleClick += new MouseButtonEventHandler(tab_MouseDoubleClick);

        // add controls to tab item, this case I added just a textbox
        StackPanel st = new StackPanel();
        st.Width = 350;
        st.Height = 350;
        Button btn = new Button();
        btn.Width = 150;
        btn.Height = 30;
        st.HorizontalAlignment = HorizontalAlignment.Center;
        st.VerticalAlignment = VerticalAlignment.Center;
        btn.Name = "txt";
        btn.Content = "Start";
        btn.Click += Btn_Click;
        Border b = new Border();
        b.BorderBrush = Brushes.Transparent;
        b.BorderThickness = new Thickness(0, 0, 0, 10);
        st.Children.Add(btn);
        st.Children.Add(b);
        st.Children.Add(pr);
        st.Orientation = Orientation.Vertical;
        tab.Content = st;

        // insert tab item right before the last (+) tab item
        _tabItems.Insert(count - 1, tab);

        return tab;
    }

    private async void Btn_Click(object sender, RoutedEventArgs e)
    {
        await Task.Run(async () =>
        {
            this.Dispatcher.Invoke(new Action(async () => {
                var progress = new Progress<int>(percent =>
                {
                    pr.Value = percent;
                });

                pr.Value = 1;

                int value = 90000;
                await Task.Run(() => DoSomeWork(value, progress));
            }), DispatcherPriority.ContextIdle);


        });
    }
    public async Task DoSomeWork(int iterations, IProgress<int> progress)
    {
        int s = 0;
        for (int i = 0; i < iterations; i++)
        {
            await Task.Run(() => {
                s *= s * i;
                progress.Report(i * 100 / iterations);
            });
        };
    }
private void tabAdd_MouseLeftButtonUp(object sender, MouseButtonEventArgs e)
    {
        // clear tab control binding
        tabDynamic.DataContext = null;

        TabItem tab = this.AddTabItem();

        // bind tab control
        tabDynamic.DataContext = _tabItems;

        // select newly added tab item
        tabDynamic.SelectedItem = tab;
    }

    private void tab_MouseDoubleClick(object sender, MouseButtonEventArgs e)
    {
        TabItem tab = sender as TabItem;

        TabProperty dlg = new TabProperty();

        // get existing header text
        dlg.txtTitle.Text = tab.Header.ToString();

        if (dlg.ShowDialog() == true)
        {
            // change header text
            tab.Header = dlg.txtTitle.Text.Trim();
        }
    }

    private void tabDynamic_SelectionChanged(object sender, SelectionChangedEventArgs e)
    {
        TabItem tab = tabDynamic.SelectedItem as TabItem;
        if (tab == null) return;

        if (tab.Equals(_tabAdd))
        {
            // clear tab control binding
            tabDynamic.DataContext = null;

            TabItem newTab = this.AddTabItem();

            // bind tab control
            tabDynamic.DataContext = _tabItems;

            // select newly added tab item
            tabDynamic.SelectedItem = newTab;
        }
        else
        {
            // your code here...
        }
    }
Posted
Updated 19-Jul-22 9:26am
Comments
Gerry Schmitz 19-Jul-22 13:40pm    
The whole scenario is unrealistic; coming up with a sensible solution is therefore also unrealistic.

1 solution

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:
C#
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:
C#
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.
C#
public partial class MainWindow : Window
{
    public MainWindow()
    {
        InitializeComponent();
    }

    // Bindable List Collection with CollectionChanged event
    ObservableCollection<TabItemViewModel> tabItems;

    public ObservableCollection<TabItemViewModel> TabItems
    {
        get
        {
            // First time showing the window
            if (tabItems is null)
            {
                tabItems = new ObservableCollection<TabItemViewModel>();
                
                // Set the Add Button to always be the last tab...
                IEditableCollectionView itemsView = (IEditableCollectionView)
                    CollectionViewSource.GetDefaultView(tabItems);

                itemsView.NewItemPlaceholderPosition =
                    NewItemPlaceholderPosition.AtEnd;
            }

            return tabItems;
        }
    }
    private void Button_Click(object sender, RoutedEventArgs e)
    {
        // set Tab details
        TabItemViewModel newTabItem = new($"Tab {tabItems.Count + 1}", 100);

        // Add tab to trigger binding update
        TabItems.Add(newTabItem);

        // Set the new tab as the Active/Visible Tab
        TabControl.SelectedItem = newTabItem;

        // Start workload
        _ = newTabItem.DoWork(); // fire and forget...
    }
}

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?
C#
public class TemplateSelector : DataTemplateSelector
{
    // Normal TabItem
    public DataTemplate ItemTemplate { get; set; }

    // Add Button TabItem
    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.
XML
<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!
 
Share this answer
 
v3

This content, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)



CodeProject, 20 Bay Street, 11th Floor Toronto, Ontario, Canada M5J 2N8 +1 (416) 849-8900