Forma koja spaja sve: batch + terminal

↑ Nazad

WorkerDemoForm: gore dugačak posao, dole terminal. Obe strane rade istovremeno i UI ostaje živ.

Šta da probaš

  1. 50.000 dokumenata → Start. Dok radi, dole pokreni powershell i kucaj git --version, dir, Get-Process.
  2. Pauza / Nastavi / Prekini.
  3. „LOŠE: posao na UI niti (5 s)": prozor se zamrzne. Zgodno za pokazivanje kolegama.

Obrazac za svaki alat

  1. async void Click handler
  2. posao u await Task.Run(...)
  3. izveštavanje preko brojača i reda koje čita tajmer (100 ms)
  4. retki događaji (proces završen) preko BeginInvoke
  5. zatvaranje forme dok posao radi: e.Cancel = true, zatraži prekid, zatvori kad se posao uredno završi

U Program.cs: Application.Run(new WorkerDemoForm()); (stari red sa Form1 je zakomentarisan).

WorkerDemoForm.cs

using System.Diagnostics;

namespace WinFormsApp_test
{
    /// <summary>
    ///  Gore: dugačak posao u pozadini (progress, log, pauza, prekid).
    ///  Dole: živ cmd/powershell — komande idu u proces, izlaz se vraća u prozor, dok posao gore radi.
    ///
    ///  Pravilo: UI nit SAMO crta i reaguje na klik. Sve teško ide u Task.Run.
    ///  Pozadina → UI ide na dva načina:
    ///   1) česte stvari (log, progres): pozadina upisuje u red/brojač, uiTimer (100 ms) prikazuje
    ///   2) retke stvari (shell ugašen): BeginInvoke
    /// </summary>
    public partial class WorkerDemoForm : Form
    {
        private readonly UiLogSink _jobLog;
        private readonly UiLogSink _termLog;
        private readonly Stopwatch _elapsed = new();

        private BatchJob? _job;
        private CancellationTokenSource? _cts;
        private bool _closeWhenJobEnds;

        private ShellSession? _shell;
        private readonly List<string> _history = new();
        private int _historyIndex;

        public WorkerDemoForm()
        {
            InitializeComponent();
            _jobLog = new UiLogSink(txtJobLog);
            _termLog = new UiLogSink(txtTermOut);
            cmbShell.SelectedIndex = 0;
            numParallel.Value = Math.Min(Environment.ProcessorCount, (int)numParallel.Maximum);
        }

        // ───────────────────────── Dugačak posao ─────────────────────────

        private async void btnStart_Click(object sender, EventArgs e)
        {
            _job = new BatchJob((int)numCount.Value);
            _job.Log += _jobLog.Write;          // Log stiže sa pozadinskih niti → samo u red, bez Invoke
            _cts = new CancellationTokenSource();
            int parallelism = (int)numParallel.Value;

            SetJobRunning(true);
            _jobLog.Info($"START  {_job.Total:N0} dokumenata, paralelno {parallelism}");
            _elapsed.Restart();

            try
            {
                // Task.Run = posao ide na thread pool. await = UI nit je slobodna dok se ne završi,
                // a kod POSLE await-a se opet izvršava na UI niti (zato smemo da diramo kontrole u finally).
                await Task.Run(() => _job.RunAsync(parallelism, _cts.Token));
                _jobLog.Info($"GOTOVO  ok: {_job.Done:N0}, greške: {_job.Failed:N0}, vreme: {_elapsed.Elapsed:mm\\:ss\\.f}");
            }
            catch (OperationCanceledException)
            {
                _jobLog.Info($"PREKINUTO posle {_job.Processed:N0} dokumenata");
            }
            catch (Exception ex)
            {
                _jobLog.Info("NEOČEKIVANA GREŠKA: " + ex);
            }
            finally
            {
                _elapsed.Stop();
                _cts.Dispose();
                _cts = null;
                UpdateProgress();
                SetJobRunning(false);
            }

            if (_closeWhenJobEnds) Close();
        }

        private void btnPause_Click(object sender, EventArgs e)
        {
            if (_job == null) return;

            if (_job.IsPaused)
            {
                _job.Resume();
                _elapsed.Start();
                _jobLog.Info("NASTAVAK");
            }
            else
            {
                _job.Pause();
                _elapsed.Stop();
                _jobLog.Info("PAUZA (radnici završavaju dokument na kome su)");
            }
            btnPause.Text = _job.IsPaused ? "Nastavi" : "Pauza";
        }

        private void btnCancel_Click(object sender, EventArgs e)
        {
            _cts?.Cancel();          // GUI → posao: token javlja svim radnicima da stanu
            btnCancel.Enabled = false;
        }

        private void SetJobRunning(bool running)
        {
            btnStart.Enabled = !running;
            btnBlockUi.Enabled = !running;
            numCount.Enabled = !running;
            numParallel.Enabled = !running;
            btnPause.Enabled = running;
            btnCancel.Enabled = running;
            btnPause.Text = "Pauza";
        }

        private void UpdateProgress()
        {
            if (_job == null) return;

            int processed = _job.Processed;
            progressBar.Maximum = _job.Total;
            progressBar.Value = Math.Min(processed, _job.Total);

            double seconds = _elapsed.Elapsed.TotalSeconds;
            double rate = seconds > 0 ? processed / seconds : 0;
            string eta = rate > 0 ? TimeSpan.FromSeconds((_job.Total - processed) / rate).ToString(@"hh\:mm\:ss") : "--:--:--";

            lblStatus.Text = $"{processed:N0} / {_job.Total:N0}  ({100.0 * processed / _job.Total:F1}%)    " +
                             $"greške: {_job.Failed:N0}    {rate:N0} dok/s    proteklo {_elapsed.Elapsed:hh\\:mm\\:ss}    ETA {eta}" +
                             (_job.IsPaused ? "    [PAUZA]" : "");
        }

        /// <summary>
        ///  Ovako NE: posao direktno u Click handleru. Prozor se zamrzne ("Not responding"),
        ///  progress bar i log se ne osvežavaju, terminal dole ne reaguje — sve dok se ne završi.
        /// </summary>
        private void btnBlockUi_Click(object sender, EventArgs e)
        {
            progressBar.Maximum = 50;
            for (int i = 1; i <= 50; i++)
            {
                Thread.Sleep(100);                     // "obrada" na UI niti
                progressBar.Value = i;                 // ne vidi se — UI nit nema kad da crta
                _jobLog.Info($"UI nit radi korak {i}/50");
            }
            lblStatus.Text = "UI nit je bila blokirana 5 s.";
        }

        // ───────────────────────── Terminal ─────────────────────────

        private void btnShellStart_Click(object sender, EventArgs e)
        {
            _shell?.Dispose();

            string shellName = (string)cmbShell.SelectedItem!;
            string workDir = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), "source", "repos");
            if (!Directory.Exists(workDir)) workDir = Environment.GetFolderPath(Environment.SpecialFolder.UserProfile);

            try
            {
                _shell = ShellSession.Start(shellName, workDir);
            }
            catch (Exception ex)
            {
                _termLog.Write($"!! {shellName} ne može da se pokrene: {ex.Message}");
                return;
            }

            ShellSession shell = _shell;
            shell.Output += _termLog.Write;     // proces → GUI: ide u red, uiTimer prikazuje
            shell.Exited += code =>             // retko → može direktno BeginInvoke
            {
                if (IsDisposed || !IsHandleCreated) return;
                BeginInvoke(() =>
                {
                    _termLog.Write($"── {shell.ShellName} završen (exit {code}) ──");
                    if (_shell == shell) SetShellRunning(false);
                });
            };

            _termLog.Write($"── {shellName} pokrenut u {workDir} ──");
            SetShellRunning(true);
            txtInput.Focus();
        }

        private void btnShellKill_Click(object sender, EventArgs e)
        {
            _shell?.Kill();      // ubija i sve što je shell pokrenuo (npr. "ping -t")
        }

        private void txtInput_KeyDown(object sender, KeyEventArgs e)
        {
            switch (e.KeyCode)
            {
                case Keys.Enter:
                    e.SuppressKeyPress = true;   // bez "ding" zvuka
                    SendCommand(txtInput.Text);
                    break;

                case Keys.Up when _history.Count > 0:
                    e.SuppressKeyPress = true;
                    _historyIndex = Math.Max(0, _historyIndex - 1);
                    ShowHistory();
                    break;

                case Keys.Down when _history.Count > 0:
                    e.SuppressKeyPress = true;
                    _historyIndex = Math.Min(_history.Count, _historyIndex + 1);
                    ShowHistory();
                    break;
            }
        }

        private void SendCommand(string command)
        {
            if (_shell is not { IsRunning: true })
            {
                _termLog.Write("!! shell nije pokrenut — klikni 'Pokreni'");
                return;
            }

            _termLog.Write("> " + command);
            _shell.Send(command);                // GUI → proces: upis u stdin

            if (command.Length > 0 && (_history.Count == 0 || _history[^1] != command))
                _history.Add(command);
            _historyIndex = _history.Count;
            txtInput.Clear();
        }

        private void ShowHistory()
        {
            txtInput.Text = _historyIndex < _history.Count ? _history[_historyIndex] : "";
            txtInput.SelectionStart = txtInput.TextLength;
        }

        private void SetShellRunning(bool running)
        {
            btnShellKill.Enabled = running;
            cmbShell.Enabled = !running;
            btnShellStart.Text = running ? "Restart" : "Pokreni";
            lblShellState.Text = running ? $"{_shell?.ShellName} radi" : "nije pokrenut";
        }

        // ───────────────────────── Zajedničko ─────────────────────────

        /// <summary>Jedino mesto gde se log i progres crtaju — 10 puta u sekundi, koliko god da posao radi.</summary>
        private void uiTimer_Tick(object sender, EventArgs e)
        {
            _jobLog.Flush();
            _termLog.Flush();
            if (_cts != null) UpdateProgress();
        }

        private void WorkerDemoForm_FormClosing(object sender, FormClosingEventArgs e)
        {
            // Posao još radi → zatraži prekid, zatvori prozor tek kad se posao uredno završi
            if (_cts != null)
            {
                e.Cancel = true;
                _closeWhenJobEnds = true;
                _job?.Resume();
                _cts.Cancel();
                return;
            }
            _shell?.Dispose();
        }
    }
}

WorkerDemoForm.Designer.cs je standardan dizajnerski fajl (SplitContainer, dva FlowLayoutPanel-a, ProgressBar, TextBox-ovi, Timer na 100 ms) i otvara se u Visual Studio dizajneru.

Izvor: 07-forma-koja-spaja.md