简体   繁体   中英

WPF Progress Bar - Update Progress from ClickOnce Upgrade

I have been deploying updates for an application of mine with ClickOnce for a while. While I'm happy to be able to make improvements, I'm a little frustrated with the current progress bar. A little background - I have a XAML window class called "UpdateProgress" that I open when an update is being undertaken for the application. Here's the current code snippet I'm using right now, which does at least notify the user that progress is being made without freezing the application/crashing, but DOES NOT visually update the progress bar:

case UpdateStatuses.UpdateAvailable:
    DialogResult dialogResult = System.Windows.Forms.MessageBox.Show("An update is available. Would you like to update the application now?", "Update available", MessageBoxButtons.OKCancel);
    if (dialogResult.ToString() == "OK")
    {
        BackgroundWorker bgUpdate = new BackgroundWorker();
        UpdateProgress updateNotify = new UpdateProgress();
        bgUpdate.WorkerReportsProgress = true;
        bgUpdate.DoWork += (uptSender, uptE) => { UpdateApplication();};
        bgUpdate.ProgressChanged += (progSender, progE) => { updateNotify.updateProgress.Value = progE.ProgressPercentage; };
        bgUpdate.RunWorkerCompleted += (comSender, comE) => {
            updateNotify.Close();
            applicationUpdated();
        };
        updateNotify.Show();
        bgUpdate.RunWorkerAsync();
    }
    break;

Basically, I'm creating a background worker above, which runs the code below:

private static void UpdateApplication()
{
    try
    {
        ApplicationDeployment updateCheck = ApplicationDeployment.CurrentDeployment;
        //BackgroundWorker bgWorker = new BackgroundWorker();
        //UpdateProgress updateNotify = new UpdateProgress();
        //updateCheck.UpdateProgressChanged += (s, e) =>
        //{
        //    updateNotify.updateProgress.Value = e.ProgressPercentage;

        //};
        //bgWorker.RunWorkerCompleted += new RunWorkerCompletedEventHandler(UpdateComponent.noteUpdates);
        //bgWorker.RunWorkerAsync();
        //updateCheck.UpdateCompleted += (s, e) =>
        //{
        //   updateNotify.Close();
        //    applicationUpdated();

        //};

        //updateNotify.Dispatcher.InvokeAsync(() =>
        // {
             //updateNotify.Show();
             updateCheck.Update();
        //});
        //return null;
    }
    catch (DeploymentDownloadException dde)
    {
        System.Windows.MessageBox.Show("Cannot install the latest version of the application. Please check your network connection, or try again later. Error: " + dde);
        //return null;
    }
}

Quick explanation, currently I'm only creating an "ApplicationDeployment" instance called "updateCheck" and just having it run the update in this thread. What I've tried attempting before, is loading some of the commented code below, only to see the application crash when updating. Turns out, when debugging with a PROD instance of my application, it's due to the following error:

The calling thread cannot access this object because a different thread owns it.

Now, doing some digging, I've seen quite a few good reads about this. From what I understand, part of the problem is that I'm trying to run this code from a static class separated from my MainWindow and other UI classes. I'm doing this to try to keep my code clean and modular, but apparently, that comes with a price. I realize that one can bind the progress bar's progress percentage if it's in the code-behind of, say, the progress bar's window, but what if I'm trying to stick to running this in the static class I speak of instead? I've tried using things like the Dispatcher methods/BeginInvoke(), but unfortunately to end up with the same result.

Can someone give me the best suggestion on how to update the progress of my progress bar in a window with the percentage progress of an ApplicationDeployment instance's update routine?

Thanks a super ton in advance.

You're mis understanding the cause of your error. Any UI control should be updated from the thread that owns it.

First what you need to wrap is only the line of code that updates your progress bar.

Then you have two ways to wrap your call, either using IProgress<T> or Dispatcher . The former being quite cool as basically you're invoking an Action<T> and Progress<T> ensures to run it in the synchronization context it was instantiated, eg the UI thread. This is nice as basically you're abstracting things VS directly using the WPF Dispatcher.

Two really different approaches here, first is declared at caller then callee calls its Report method, second effectively wraps the call to UI in callee.

That's what you are executing during bgUpdate.ProgressChanged that needs to be taken care of.

And now if I were you I'd ditch BackgroundWorker in favor of Task since it's the preferred way to do that now, especially in WPF.

Smallest example using Task and IProgress :

Code:

<Window x:Class="WpfApp1.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"
        mc:Ignorable="d">
    <StackPanel>
        <Button Content="DoWork" Click="Button1_Click" />
        <ProgressBar Height="20" x:Name="ProgressBar1" Maximum="1.0"/>
    </StackPanel>
</Window>

Code:

using System;
using System.Threading.Tasks;
using System.Windows;

namespace WpfApp1
{
    public partial class MainWindow
    {
        public MainWindow()
        {
            InitializeComponent();
        }

        private async void Button1_Click(object sender, RoutedEventArgs e)
        {
            var progress = new Progress<double>(s => { ProgressBar1.Value = s; });

            await Task.Run(() => DoWork(progress));
        }

        private static async Task DoWork(IProgress<double> progress = null)
        {
            const int count = 100;

            for (var i = 0; i < count; i++)
            {
                await Task.Delay(50);

                progress?.Report(1.0d / (count - 1) * i);
            }
        }
    }
}

在此处输入图片说明

Now you're code doesn't even need to know about Dispatcher that is WPF-specific, code could be anywhere, update any framework.

You could also cancel the operation with Task.Run :

https://docs.microsoft.com/en-us/dotnet/api/system.threading.tasks.task.run?view=netframework-4.7.2

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM