Показывать индикатор выполнения при выполнении какой-либо работы в C #?

Что такое NullPointerException?

Хорошим местом для начала является JavaDocs . Они охватывают это:

Брошено, когда приложение пытается использовать null в случае, когда требуется объект. К ним относятся:

  • Вызов метода экземпляра нулевого объекта.
  • Доступ или изменение поля нулевого объекта.
  • Выполнение длины null, как если бы это был массив.
  • Доступ или изменение слотов с нулевым значением, как если бы это был массив.
  • Бросать нуль, как если бы это было значение Throwable.

Приложения должны бросать экземпляры этого класса для указания других незаконных видов использования нулевого объекта.

Также, если вы попытаетесь использовать нулевую ссылку с synchronized, который также выдаст это исключение, за JLS :

SynchronizedStatement:
    synchronized ( Expression ) Block
  • В противном случае, если значение выражения равно null, NullPointerException.

Как это исправить?

Итак, у вас есть NullPointerException. Как вы это исправите? Возьмем простой пример, который выдает NullPointerException:

public class Printer {
    private String name;

    public void setName(String name) {
        this.name = name;
    }

    public void print() {
        printString(name);
    }

    private void printString(String s) {
        System.out.println(s + " (" + s.length() + ")");
    }

    public static void main(String[] args) {
        Printer printer = new Printer();
        printer.print();
    }
}

Идентифицирует нулевые значения

. Первый шаг - точно определить , значения которого вызывают исключение . Для этого нам нужно выполнить некоторую отладку. Важно научиться читать stacktrace . Это покажет вам, где было выбрано исключение:

Exception in thread "main" java.lang.NullPointerException
    at Printer.printString(Printer.java:13)
    at Printer.print(Printer.java:9)
    at Printer.main(Printer.java:19)

Здесь мы видим, что исключение выбрано в строке 13 (в методе printString). Посмотрите на строку и проверьте, какие значения равны нулю, добавив протоколирующие операторы или используя отладчик . Мы обнаруживаем, что s имеет значение null, а вызов метода length на него вызывает исключение. Мы видим, что программа перестает бросать исключение, когда s.length() удаляется из метода.

Трассировка, где эти значения взяты из

Затем проверьте, откуда это значение. Следуя вызовам метода, мы видим, что s передается с printString(name) в методе print(), а this.name - null.

Трассировка, где эти значения должны быть установлены

Где установлен this.name? В методе setName(String). С некоторой дополнительной отладкой мы видим, что этот метод вообще не вызывается. Если этот метод был вызван, обязательно проверьте порядок , что эти методы вызывают, а метод set не будет называться после методом печати. ​​

Этого достаточно, чтобы дать нам решение: добавить вызов printer.setName() перед вызовом printer.print().

Другие исправления

Переменная может иметь значение по умолчанию setName может помешать ему установить значение null):

private String name = "";

Либо метод print, либо printString может проверить значение null например:

printString((name == null) ? "" : name);

Или вы можете создать класс, чтобы name всегда имел ненулевое значение :

public class Printer {
    private final String name;

    public Printer(String name) {
        this.name = Objects.requireNonNull(name);
    }

    public void print() {
        printString(name);
    }

    private void printString(String s) {
        System.out.println(s + " (" + s.length() + ")");
    }

    public static void main(String[] args) {
        Printer printer = new Printer("123");
        printer.print();
    }
}

См. также:

Я все еще не могу найти проблему

Если вы попытались отладить проблему и до сих пор не имеете решения, вы можете отправить вопрос для получения дополнительной справки, но не забудьте включить то, что вы пробовали до сих пор. Как минимум, включите stacktrace в вопрос и отметьте важные номера строк в коде. Также попробуйте сначала упростить код (см. SSCCE ).

26
задан Ali 27 July 2017 в 21:11
поделиться

12 ответов

Мне кажется, что вы оперируете, по крайней мере, одним ложным предположением.

1. Вам не нужно поднимать событие ProgressChanged, чтобы иметь отзывчивый пользовательский интерфейс

В своем вопросе вы говорите так:

BackgroundWorker - это не ответ. потому что, возможно, я не понимаю. уведомление о ходе работы, что означает не будет звонка ПрогрессИзменен, так как DoWork - это одиночный вызов внешней функции . . .

На самом деле, не имеет значения, вызываете ли вы событие ProgressChanged или нет . Вся цель этого события состоит в том, чтобы временно перенести управление обратно в поток GUI, чтобы сделать обновление, которое каким-то образом отражает ход работы, выполняемой BackgroundWorker . Если вы просто отображаете шкалу прогресса, было бы на самом деле бессмысленно вообще поднимать событие ProgressChanged . Индикатор прогресса будет продолжать вращаться до тех пор, пока он отображается, поскольку BackgroundWorker[1186668] выполняет свою работу на отдельном потоке от GUI.

(На заметке DoWork - это событие, которое означает, что это не только "один вызов внешней функции"; вы можете добавить столько обработчиков, сколько захотите; и каждый из этих обработчиков может содержать столько вызовов функций, сколько захотите)

2. Вам не нужно вызывать Application.DoEvents, чтобы иметь отзывчивый UI

Для меня звучит так, как будто вы считаете, что единственный способ обновления графического интерфейса - это вызов Application.DoEvents:

Мне нужно продолжать вызывать Application.DoEvents(); для В многопоточном сценарии это не так; если вы используете BackgroundWorker, графический интерфейс будет продолжать реагировать (на своем потоке), в то время как BackgroundWorker будет делать то, что было прикреплено к событию DoWork. Ниже приведен простой пример того, как это может сработать для вас.

private void ShowProgressFormWhileBackgroundWorkerRuns() {
    // this is your presumably long-running method
    Action<string, string> exec = DoSomethingLongAndNotReturnAnyNotification;

    ProgressForm p = new ProgressForm(this);

    BackgroundWorker b = new BackgroundWorker();

    // set the worker to call your long-running method
    b.DoWork += (object sender, DoWorkEventArgs e) => {
        exec.Invoke(path, parameters);
    };

    // set the worker to close your progress form when it's completed
    b.RunWorkerCompleted += (object sender, RunWorkerCompletedEventArgs e) => {
        if (p != null && p.Visible) p.Close();
    };

    // now actually show the form
    p.Show();

    // this only tells your BackgroundWorker to START working;
    // the current (i.e., GUI) thread will immediately continue,
    // which means your progress bar will update, the window
    // will continue firing button click events and all that
    // good stuff
    b.RunWorkerAsync();
}

3. Вы не можете запускать два метода одновременно на одном потоке

Вы говорите так:

Мне просто нужно вызвать Application.DoEvents() для того, чтобы Марк прогресс бар будет работать, в то время как рабочая функция работает в Главном . .

То, о чем вы просите, просто не реально -. Основным" потоком для приложения Windows Forms является поток GUI, который, если он занят вашим долгосрочным методом, не предоставляет визуальных обновлений. Если вы считаете иначе, я подозреваю, что вы неправильно понимаете, что делает BeginInvoke: он запускает делегата на отдельном потоке . На самом деле, пример кода, который вы включили в вопрос для вызова Application.DoEvents между exec.BeginInvoke и exec.EndInvoke является избыточным; на самом деле вы несколько раз вызываете Application.DoEvents из потока GUI, , который в любом случае будет обновляться . (Если вы нашли иное, я подозреваю, что это потому, что вы сразу вызвали exec.EndInvoke, который заблокировал текущий поток до тех пор, пока метод не будет завершен)

Так что да, ответ, который вы ищете, это использование BackgroundWorker.

Вы могли бы использовать BeginInvoke, но вместо вызова EndInvoke из потока GUI (который заблокирует его, если метод не будет закончен), передайте параметр AsyncCallback на ваш вызов BeginInvoke (вместо того, чтобы просто передавать null), и закройте форму прогресса в вашем обратном вызове. Однако, имейте в виду, что если вы это сделаете, вам придется вызывать метод, который закрывает форму прогресса из потока GUI, так как в противном случае вы будете пытаться закрыть форму, которая является GUI функцией, из не-GUI потока. Но на самом деле, все ловушки использования BeginInvoke/EndInvoke уже разобрались с for you with the BackgroundWorker class, даже если вы думаете, что это ".NET magic code" (для меня это просто интуитивно понятный и полезный инструмент)

.
43
ответ дан 28 November 2019 в 06:12
поделиться

Для этого мы используем модальную форму с BackgroundWorker.

Вот краткое решение:

  public class ProgressWorker<TArgument> : BackgroundWorker where TArgument : class 
    {
        public Action<TArgument> Action { get; set; }

        protected override void OnDoWork(DoWorkEventArgs e)
        {
            if (Action!=null)
            {
                Action(e.Argument as TArgument);
            }
        }
    }


public sealed partial class ProgressDlg<TArgument> : Form where TArgument : class
{
    private readonly Action<TArgument> action;

    public Exception Error { get; set; }

    public ProgressDlg(Action<TArgument> action)
    {
        if (action == null) throw new ArgumentNullException("action");
        this.action = action;
        //InitializeComponent();
        //MaximumSize = Size;
        MaximizeBox = false;
        Closing += new System.ComponentModel.CancelEventHandler(ProgressDlg_Closing);
    }
    public string NotificationText
    {
        set
        {
            if (value!=null)
            {
                Invoke(new Action<string>(s => Text = value));  
            }

        }
    }
    void ProgressDlg_Closing(object sender, System.ComponentModel.CancelEventArgs e)
    {
        FormClosingEventArgs args = (FormClosingEventArgs)e;
        if (args.CloseReason == CloseReason.UserClosing)
        {
            e.Cancel = true;
        }
    }



    private void ProgressDlg_Load(object sender, EventArgs e)
    {

    }

    public void RunWorker(TArgument argument)
    {
        System.Windows.Forms.Application.DoEvents();
        using (var worker = new ProgressWorker<TArgument> {Action = action})
        {
            worker.RunWorkerAsync();
            worker.RunWorkerCompleted += worker_RunWorkerCompleted;                
            ShowDialog();
        }
    }

    void worker_RunWorkerCompleted(object sender, System.ComponentModel.RunWorkerCompletedEventArgs e)
    {
        if (e.Error != null)
        {
            Error = e.Error;
            DialogResult = DialogResult.Abort;
            return;
        }

        DialogResult = DialogResult.OK;
    }
}

И как мы его используем:

var dlg = new ProgressDlg<string>(obj =>
                                  {
                                     //DoWork()
                                     Thread.Sleep(10000);
                                     MessageBox.Show("Background task completed "obj);
                                   });
dlg.RunWorker("SampleValue");
if (dlg.Error != null)
{
  MessageBox.Show(dlg.Error.Message, "ERROR", MessageBoxButtons.OK, MessageBoxIcon.Error);
}
dlg.Dispose();
0
ответ дан 28 November 2019 в 06:12
поделиться

Re: Ваше редактирование. Для работы вам понадобится BackgroundWorker или Thread, но он должен периодически вызывать ReportProgress(), чтобы сообщить потоку пользовательского интерфейса, что он делает. DotNet не может волшебным образом определить, сколько работы вы проделали, поэтому вы должны сказать ему (а) какой максимальный объем работы вы достигнете, а затем (б) около 100 или около того раз в течение процесса, сказать ему, какой объем работы вы выполнили. (Если вы сообщите о прогрессе менее 100 раз, то шкала прогресса будет скакать большими шагами. Если вы сообщите об этом более 100 раз, вы просто потратите время, пытаясь сообщить более тонкие детали, чем это поможет отобразить индикатор прогресса)

Если ваш поток пользовательского интерфейса может успешно продолжить работу во время работы фонового рабочего, то ваша работа выполнена.

Однако, на самом деле, в большинстве ситуаций, когда индикатор прогресса должен быть запущен, ваш пользовательский интерфейс должен быть очень осторожен, чтобы избежать повторного входящего вызова. Например, если вы запускаете индикатор прогресса во время экспорта данных, вы не хотите позволить пользователю начать экспорт данных снова во время экспорта.

Вы можете справиться с этим двумя способами:

  • Операция экспорта проверяет, запущен ли фоновый рабочий, и отключает опцию экспорта во время уже выполняемого импорта. Это позволит пользователю делать в вашей программе всё, что угодно, кроме экспорта - это всё равно может быть опасно, если пользователь сможет (например) редактировать экспортируемые данные.

  • Запустите индикатор выполнения как "модальное" отображение, чтобы ваша программа "ожила" во время экспорта, но на самом деле пользователь не сможет ничего сделать (кроме отмены), пока экспорт не завершится. DotNet - мусор для поддержки этого, хотя это наиболее распространенный подход. В этом случае вам нужно поместить поток пользовательского интерфейса в занятый цикл ожидания, где он вызывает Application.DoEvents(), чтобы сохранить обработку сообщений (так что индикатор выполнения будет работать), но вам нужно добавить MessageFilter, который позволяет вашему приложению только отвечать на "безопасные" события (например, он позволит Paint events так, чтобы окна вашего приложения продолжали перерисовываться, но он будет фильтровать сообщения мыши и клавиатуры так, чтобы пользователь на самом деле ничего не мог делать на графике во время экспорта. Также есть пара подлых сообщений, через которые вам нужно будет пройти, чтобы окно работало нормально, и их разгадка займет несколько минут - у меня есть список таких сообщений на работе, но, боюсь, здесь их не надо передавать. Это все очевидные, такие как NCHITTEST плюс подлый .net (зло в диапазоне WM_USER), который жизненно необходим для того, чтобы это заработало)

Последняя "фишка" с ужасным индикатором прогресса dotNet заключается в том, что когда вы завершаете операцию и закрываете индикатор прогресса, вы увидите, что он обычно выходит, сообщая значение типа "80%". Даже если вы надавите на 100%, а затем подождете примерно полсекунды, она все равно может не дойти до 100%. Арррррр! Решение заключается в том, чтобы установить прогресс на 100%, затем на 99%, а затем обратно на 100% - когда индикатору прогресса сказано двигаться вперед, он медленно анимируется к целевому значению. Но если сказать ему идти "назад", то он сразу же перепрыгивает на эту позицию. Таким образом, мгновенно перевернув его в конце, вы можете заставить его действительно показать то значение, которое вы его просили показать.

.
0
ответ дан 28 November 2019 в 06:12
поделиться

Читая Ваши требования, самым простым способом будет отображение безмодовой формы и использование стандартного таймера System.Windows.Forms для обновления хода работы с безмодовой формой. Никаких потоков, никаких возможных утечек памяти.

Так как при этом используется только один поток пользовательского интерфейса, то для визуального обновления индикатора прогресса необходимо в определенные моменты основной обработки вызывать Application.DoEvents().

.
0
ответ дан 28 November 2019 в 06:12
поделиться

Используйте компонент BackgroundWorker, предназначенный именно для этого сценария.

Вы можете подключиться к его событиям обновления и обновить индикатор выполнения. Класс BackgroundWorker гарантирует, что обратные вызовы будут привязаны к потоку пользовательского интерфейса, так что вам не нужно будет беспокоиться ни о каких деталях.

.
0
ответ дан 28 November 2019 в 06:12
поделиться

BackgroundWorker - это не ответ, потому что, возможно, я не получаю уведомление о прогрессе...

Какое отношение имеет тот факт, что вы не получаете уведомление о прогрессе, к использованию BackgroundWorker? Если ваша долгосрочная задача не имеет надежного механизма оповещения о своем прогрессе, то достоверно сообщить о нем невозможно.

Самый простой способ оповещения о прогрессе долгосрочного метода - это запустить метод в потоке пользовательского интерфейса и заставить его сообщить о прогрессе, обновив индикатор прогресса, а затем вызвать Application.DoEvents(). Технически это сработает. Однако пользовательский интерфейс не будет реагировать между вызовами Application.DoEvents(). Это быстрое и грязное решение, и, как заметил Стив МакКоннелл, проблема с быстрыми и грязными решениями заключается в том, что горечь грязи остается еще долго после того, как сладость быстрого забыта.

Следующим простейшим способом, на который ссылается другой плакат, является реализация модальной формы, использующей BackgroundWorker для выполнения долговременного метода. Это обеспечивает в целом лучший пользовательский опыт, и избавляет вас от необходимости решать потенциально сложную проблему того, какие части вашего пользовательского интерфейса должны оставаться функциональными во время выполнения длительной задачи - пока модальная форма открыта, ни одна из остальных частей вашего пользовательского интерфейса не будет реагировать на действия пользователя. Это быстрое и чистое решение.

Но оно все еще довольно враждебно по отношению к пользователю. Он все еще блокирует пользовательский интерфейс во время выполнения длительной задачи; он просто делает это красивым способом. Чтобы сделать решение удобным для пользователя, нужно выполнить задачу в другом потоке. Самый простой способ сделать это - с помощью BackgroundWorker.

Этот подход открывает двери для множества проблем. Он не "просочится", что бы это ни значило. Но что бы ни делал этот давно работающий метод, теперь он должен делать это в полной изоляции от кусочков пользовательского интерфейса, которые остаются включенными во время работы. И под полным, я имею в виду полным. Если пользователь может щелкнуть мышью где угодно и вызвать обновление какого-нибудь объекта, на который когда-либо смотрит ваш долгоработающий метод, то у вас будут проблемы. Любой объект, используемый вашим долгосрочным методом, который может вызвать событие, является потенциальной дорогой к несчастью.

Именно это, а не получение BackgroundWorker для корректной работы, станет источником всей этой боли.

.
2
ответ дан 28 November 2019 в 06:12
поделиться

Действительно, вы на правильном пути. Вы должны использовать другой поток, и вы определили лучшие пути для этого. Остальное - просто обновление индикатора прогресса. В случае, если вы не хотите использовать BackgroundWorker, как это предлагали другие, есть один трюк, который следует помнить. Фокус в том, что вы не можете обновить шкалу прогресса из рабочего потока, потому что пользовательским интерфейсом можно манипулировать только из рабочего потока. Поэтому вы используете метод Invoke. Он идет примерно так (исправьте синтаксические ошибки сами, я просто пишу небольшой пример):

class MyForm: Form
{
    private void delegate UpdateDelegate(int Progress);

    private void UpdateProgress(int Progress)
    {
        if ( this.InvokeRequired )
            this.Invoke((UpdateDelegate)UpdateProgress, Progress);
        else
            this.MyProgressBar.Progress = Progress;
    }
}

Свойство InvokeRequired вернет true для каждого потока, кроме того, который владеет формой. Метод Invoke вызовет метод в потоке UI и будет блокировать его до завершения. Если не нужно блокировать, можно вызвать BeginInvoke.

.
3
ответ дан 28 November 2019 в 06:12
поделиться

С помощью опции . NET/C# на Stackoverflow, но статья, которая очистила для меня нити оконных форм, была нашим резидентным оракулом Джоном Скитом "Threading in Windows Forms".

Вся серия стоит прочитать, чтобы почистить свои знания или научиться с нуля.

Я нетерпелив, просто покажите мне какой-нибудь код

Что касается "покажите мне код", то ниже мы рассмотрим, как бы я это делал с C# 3.5. Форма содержит 4 элемента управления:

  • текстовое поле
  • a progressbar
  • 2 кнопки: "buttonLongTask" и "buttonAnother"

buttonAnother существует чисто для того, чтобы показать, что пользовательский интерфейс не блокируется во время выполнения задачи счета до 100.

public partial class Form1 : Form
{
    public Form1()
    {
        InitializeComponent();
    }

    private void buttonLongTask_Click(object sender, EventArgs e)
    {
        Thread thread = new Thread(LongTask);
        thread.IsBackground = true;
        thread.Start();
    }

    private void buttonAnother_Click(object sender, EventArgs e)
    {
        textBox1.Text = "Have you seen this?";
    }

    private void LongTask()
    {
        for (int i = 0; i < 100; i++)
        {
            Update1(i);
            Thread.Sleep(500);
        }
    }

    public void Update1(int i)
    {
        if (InvokeRequired)
        {
            this.BeginInvoke(new Action<int>(Update1), new object[] { i });
            return;
        }

        progressBar1.Value = i;
    }
}
10
ответ дан 28 November 2019 в 06:12
поделиться

Для меня самым простым способом определенно является использование BackgroundWorker , который специально разработан для такого рода задач. Событие ProgressChanged идеально подходит для обновления индикатора прогресса, не беспокоясь о звонках с перекрестными резьбами

.
16
ответ дан 28 November 2019 в 06:12
поделиться

Я должен бросить самый простой ответ. Всегда можно просто реализовать шкалу прогресса и не иметь никакого отношения ни к чему реальному прогрессу. Просто начните заполнять бар, скажем 1% в секунду, или 10% в секунду, что-то похожее на ваше действие, и если оно заполнится заново, то начнется заново.

Это придаст пользователю видимость обработки и заставит его понять, что нужно подождать, вместо того, чтобы просто нажать кнопку и увидеть, что ничего не происходит, а затем нажать еще раз

.
1
ответ дан 28 November 2019 в 06:12
поделиться

И еще один пример, что BackgroundWorker - правильный способ...

using System;
using System.ComponentModel;
using System.Threading;
using System.Windows.Forms;

namespace SerialSample
{
    public partial class Form1 : Form
    {
        private BackgroundWorker _BackgroundWorker;
        private Random _Random;

        public Form1()
        {
            InitializeComponent();
            _ProgressBar.Style = ProgressBarStyle.Marquee;
            _ProgressBar.Visible = false;
            _Random = new Random();

            InitializeBackgroundWorker();
        }

        private void InitializeBackgroundWorker()
        {
            _BackgroundWorker = new BackgroundWorker();
            _BackgroundWorker.WorkerReportsProgress = true;

            _BackgroundWorker.DoWork += (sender, e) => ((MethodInvoker)e.Argument).Invoke();
            _BackgroundWorker.ProgressChanged += (sender, e) =>
                {
                    _ProgressBar.Style = ProgressBarStyle.Continuous;
                    _ProgressBar.Value = e.ProgressPercentage;
                };
            _BackgroundWorker.RunWorkerCompleted += (sender, e) =>
            {
                if (_ProgressBar.Style == ProgressBarStyle.Marquee)
                {
                    _ProgressBar.Visible = false;
                }
            };
        }

        private void buttonStart_Click(object sender, EventArgs e)
        {
            _BackgroundWorker.RunWorkerAsync(new MethodInvoker(() =>
                {
                    _ProgressBar.BeginInvoke(new MethodInvoker(() => _ProgressBar.Visible = true));
                    for (int i = 0; i < 1000; i++)
                    {
                        Thread.Sleep(10);
                        _BackgroundWorker.ReportProgress(i / 10);
                    }
                }));
        }
    }
}
9
ответ дан 28 November 2019 в 06:12
поделиться

Если вам нужен "вращающийся" индикатор прогресса, почему бы не установить стиль индикатора прогресса на "Marquee" и использовать BackgroundWorker, чтобы пользовательский интерфейс оставался отзывчивым? Вы не сможете достичь вращающегося прогресса легче, чем при использовании стиля "Marquee"...

.
0
ответ дан 28 November 2019 в 06:12
поделиться
Другие вопросы по тегам:

Похожие вопросы: