Home > OS >  Xceed WPF Toolkit - BusyIndicator - creating dynamic loading messages in C#, data binding in a data
Xceed WPF Toolkit - BusyIndicator - creating dynamic loading messages in C#, data binding in a data

Time:09-16

I have a small WPF app that I am working on that uses the Xceed BusyIndicator. I'm having some trouble dynamically updating the loading message because the content is contained within a DataTemplate. The methods I'm familiar with for data binding, or setting the value of text aren't working.

I've done some research - and it looks like others have had this issue. It seems it was answered here, but because the answer was not in context I cannot quite figure out how that would work in my code.

Here is my sample code, if someone could help me understand what I'm missing I would greatly appreciate it. This has the added challenge of using a BackgroundWorker thread. I use this because I anticipate this will be a long running progress - ultimately the action will start a SQL Job that will process items that may take up to 15 minutes. My plan is to have the thread periodically run a stored procedure to get a count of remaining items to process and update the loading message.

MainWindow.xaml:

<Window x:Class="WPFTest.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:xctk="http://schemas.xceed.com/wpf/xaml/toolkit"
        xmlns:local="clr-namespace:WPFTest"
        mc:Ignorable="d"
        Title="MainWindow" Height="450" Width="800">

    <xctk:BusyIndicator x:Name="AutomationIndicator">
        <xctk:BusyIndicator.BusyContentTemplate>
            <DataTemplate>
                <StackPanel Margin="4">
                    <TextBlock Text="Sending Invoices" FontWeight="Bold" HorizontalAlignment="Center"/>
                        <WrapPanel>
                            <TextBlock Text="Items remaining: "/>
                        <TextBlock x:Name="_ItemsRemaining" Text="{Binding Path=DataContext.ItemsRemaining, RelativeSource={RelativeSource Mode=FindAncestor, AncestorType=Window}}"/>
                        </WrapPanel>
                    

                </StackPanel>
            </DataTemplate>
        </xctk:BusyIndicator.BusyContentTemplate>
        <Grid>
            <StackPanel>
                <TextBlock Text="Let's test this thing" />
                <Button x:Name="_testBtn" Content="Start" Width="100" HorizontalAlignment="Left" Click="testBtn_Click"/>
                <TextBlock Text="{Binding ItemsRemaining}"/>
            </StackPanel>
        </Grid>
    </xctk:BusyIndicator>
</Window>

MainWindow.xaml.cs:

using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Linq;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Data;
using System.Windows.Documents;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Media.Imaging;
using System.Windows.Navigation;
using System.Windows.Shapes;

namespace WPFTest
{
    /// <summary>
    /// Interaction logic for MainWindow.xaml
    /// </summary>
    public partial class MainWindow : Window
    {
        
        public MainWindow()
        {
            InitializeComponent();
            UpdateItemsRemaining(0);
        }


        public class ItemCountDown
        {
            //One Idea was to try and set a data binding variable
            public string ItemsRemaining { get; set; }
        }

        public void UpdateItemsRemaining(int n)
        {
            ItemCountDown s = new ItemCountDown();
            {
                s.ItemsRemaining = n.ToString();
            };
            //this.AutomationIndicator.DataContext = s;      Works during initiation, but not in the thread worker.



    }


        private void testBtn_Click(object sender, RoutedEventArgs e)
        {
            //Someone clicked the button, run the Test Status 
            TestStatus();
        }


        public void TestStatus()
        {
            // Normally I'd start a background worker to run a loop to check that status in SQL
            BackgroundWorker getStatus = new BackgroundWorker();
            getStatus.DoWork  = (o, ea) =>
            {

                //Normally there's a sql connection being opened to check a SQL Job, and then I run a loop that opens the connection to check
                //the status until it either fails or successfully ended.  

                //but for this test, I'll just have it run for 15 seconds, counting down fake items.

                int fakeItems = 8;

                do  //
                {

                    //Idea One - write to the text parameter.  But can't find it in the template
                    //Dispatcher.Invoke((Action)(() => _ItemsRemaining.Text = fakeItems));

                    //Idea two - use data binding to update the value.  Data binding works just find outside of the Data Template but is ignored in the template
                    UpdateItemsRemaining(fakeItems);


                    //subtract one from fake items and wait a second.
                    fakeItems--;
                    Thread.Sleep(1000);

                } while (fakeItems > 0);


            };

            getStatus.RunWorkerCompleted  = (o, ea) =>
            {
                //work done, end it.
                AutomationIndicator.IsBusy = false;

            };

            AutomationIndicator.IsBusy = true;
            getStatus.RunWorkerAsync();

        }
    }
}

Thank you for reviewing and I appreciate any help or direction given.

CodePudding user response:

Set the DataContext to your ItemCountDown object and implement INotifyPropertyChanged:

public class ItemCountDown : INotifyPropertyChanged
{
    private string _itemsRemaining;
    public string ItemsRemaining
    {
        get { return _itemsRemaining; }
        set { _itemsRemaining = value; NotifyPropertyChanged(); }
    }

    public event PropertyChangedEventHandler PropertyChanged;
    private void NotifyPropertyChanged([CallerMemberName] String propertyName = "") =>
        PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}

public partial class MainWindow : Window
{
    private readonly ItemCountDown s = new ItemCountDown();

    public MainWindow()
    {
        InitializeComponent();
        DataContext = s;
        UpdateItemsRemaining(0);
    }

    public void UpdateItemsRemaining(int n)
    {
        s.ItemsRemaining = n.ToString();
    }


    private void testBtn_Click(object sender, RoutedEventArgs e)
    {
        TestStatus();
    }

    public void TestStatus()
    {
        ...
    }
}

You can then bind directly to the property in the DataTemplate of the XAML markup:

<TextBlock x:Name="_ItemsRemaining" Text="{Binding Path=DataContext.ItemsRemaining, RelativeSource={RelativeSource Mode=FindAncestor, AncestorType=Window}}"/>
  • Related