diff --git a/Glyssen/Dialogs/GenerateGroupsProgressDialog.cs b/Glyssen/Dialogs/GenerateGroupsProgressDialog.cs
index 9e75b7f7f..de30e6667 100644
--- a/Glyssen/Dialogs/GenerateGroupsProgressDialog.cs
+++ b/Glyssen/Dialogs/GenerateGroupsProgressDialog.cs
@@ -1,6 +1,8 @@
using System;
using System.ComponentModel;
using System.Linq;
+using System.Threading;
+using System.Threading.Tasks;
using System.Windows.Forms;
using GlyssenEngine;
using GlyssenEngine.Bundle;
@@ -13,10 +15,12 @@ namespace Glyssen.Dialogs
{
class GenerateGroupsProgressDialog : ProgressDialogWithAcknowledgement
{
+ private readonly CharacterGroupGenerator m_generator;
private readonly string m_sizeInfo;
- public GenerateGroupsProgressDialog(Project project, DoWorkEventHandler doWorkEventHandler, bool firstRun, bool replaceCancelButtonWithLink = false)
+ public GenerateGroupsProgressDialog(Project project, CharacterGroupGenerator generator, bool firstRun, bool replaceCancelButtonWithLink = false)
{
+ m_generator = generator;
InitializeComponent();
Text = LocalizationManager.GetString("DialogBoxes.GenerateGroupsProgressDialog.Title", "Generating Groups");
@@ -60,18 +64,18 @@ public GenerateGroupsProgressDialog(Project project, DoWorkEventHandler doWorkEv
ProgressLabelTextWhenComplete = LocalizationManager.GetString("DialogBoxes.GenerateGroupsProgressDialog.Complete",
"Group generation is complete.");
BarStyle = ProgressBarStyle.Marquee;
- BackgroundWorker worker = new BackgroundWorker();
- worker.WorkerSupportsCancellation = true;
- worker.DoWork += doWorkEventHandler;
- BackgroundWorker = worker;
}
- protected override void OnBackgroundWorker_RunWorkerCompleted(object sender, RunWorkerCompletedEventArgs e)
+ protected override void DoWork(CancellationToken cancellationToken)
{
- if (e.Error != null)
+ try
+ {
+ m_generator.GenerateCharacterGroups(cancellationToken);
+ }
+ catch
{
if (!CanCancel)
- throw e.Error;
+ throw;
var msg = LocalizationManager.GetString("DialogBoxes.GenerateGroupsProgressDialog.GenerationFailed",
"New character groups could not be generated to satisfy the project settings for the current cast size. ({0})",
"Parameter is a statement about the number of voice actors or planned cast size.");
@@ -79,8 +83,6 @@ protected override void OnBackgroundWorker_RunWorkerCompleted(object sender, Run
DialogResult = DialogResult.Cancel;
Close();
}
- else
- base.OnBackgroundWorker_RunWorkerCompleted(sender, e);
}
private void InitializeComponent()
@@ -100,11 +102,10 @@ public static void GenerateGroupsWithProgress(Project project, bool attemptToPre
if (forceMatchToActors)
project.CharacterGroupGenerationPreferences.CastSizeOption = CastSizeOption.MatchVoiceActorList;
bool saveGroups = false;
- using (var progressDialog = new GenerateGroupsProgressDialog(project, OnGenerateGroupsWorkerDoWork, firstGroupGenerationRun, cancelLink))
- {
- var generator = new CharacterGroupGenerator(project, ghostCastSize, progressDialog.BackgroundWorker);
- progressDialog.ProgressState.Arguments = generator;
+ var generator = new CharacterGroupGenerator(project, ghostCastSize);
+ using (var progressDialog = new GenerateGroupsProgressDialog(project, generator, firstGroupGenerationRun, cancelLink))
+ {
if (progressDialog.ShowDialog() == DialogResult.OK && generator.GeneratedGroups != null)
{
var assignedBefore = project.CharacterGroupList.CountVoiceActorsAssigned();
@@ -124,11 +125,5 @@ public static void GenerateGroupsWithProgress(Project project, bool attemptToPre
}
project.Save(saveGroups);
}
-
- private static void OnGenerateGroupsWorkerDoWork(object s, DoWorkEventArgs e)
- {
- var generator = (CharacterGroupGenerator)((ProgressState)e.Argument).Arguments;
- generator.GenerateCharacterGroups();
- }
}
}
diff --git a/Glyssen/Dialogs/ProgressDialogWithAcknowledgement.cs b/Glyssen/Dialogs/ProgressDialogWithAcknowledgement.cs
index 2b6f53bab..6aef5c4db 100644
--- a/Glyssen/Dialogs/ProgressDialogWithAcknowledgement.cs
+++ b/Glyssen/Dialogs/ProgressDialogWithAcknowledgement.cs
@@ -5,29 +5,30 @@
using System.Diagnostics;
using System.Drawing;
using System.Globalization;
+using System.Threading;
+using System.Threading.Tasks;
using System.Windows.Forms;
using L10NSharp;
using SIL.Progress;
using SIL.Reporting;
+using Timer = System.Windows.Forms.Timer;
namespace Glyssen.Dialogs
{
///
- /// Provides a progress dialog which forces the user to acknowledge is complete by clicking OK
+ /// Provides a progress dialog which forces the user to acknowledge completion by clicking OK
///
- public class ProgressDialogWithAcknowledgement : Form
+ public class ProgressDialogWithAcknowledgement : Form, IProgress
{
public delegate void ProgressCallback(int progress);
private Label m_statusLabel;
private ProgressBar m_progressBar;
private Label m_progressLabel;
- private Timer m_showWindowIfTakingLongTimeTimer;
private Timer m_progressTimer;
private Label m_overviewLabel;
private DateTime m_startTime;
- private BackgroundWorker m_backgroundWorker;
- private ProgressState m_progressState;
+ private CancellationTokenSource m_cancellationTokenSource;
private TableLayoutPanel m_tableLayout;
private bool m_workerStarted;
private Button m_okButton;
@@ -92,14 +93,8 @@ private void HandleTableLayoutSizeChanged(object sender, EventArgs e)
///
public string StatusText
{
- get
- {
- return m_statusLabel.Text;
- }
- set
- {
- m_statusLabel.Text = value;
- }
+ get => m_statusLabel.Text;
+ set => m_statusLabel.Text = value;
}
///
@@ -107,49 +102,27 @@ public string StatusText
///
public string Overview
{
- get
- {
- return m_overviewLabel.Text;
- }
- set
- {
- m_overviewLabel.Text = value;
- }
+ get => m_overviewLabel.Text;
+ set => m_overviewLabel.Text = value;
}
///
/// Get / set the minimum range of the progress bar
///
- public int ProgressRangeMinimum
+ private int ProgressRangeMinimum
{
- get
- {
- return m_progressBar.Minimum;
- }
- set
- {
- if (m_backgroundWorker == null)
- {
- m_progressBar.Minimum = value;
- }
- }
+ get => m_progressBar.Minimum;
+ set => m_progressBar.Minimum = value;
}
///
/// Get / set the maximum range of the progress bar
///
- public int ProgressRangeMaximum
+ private int ProgressRangeMaximum
{
- get
- {
- return m_progressBar.Maximum;
- }
+ get => m_progressBar.Maximum;
set
{
- if (m_backgroundWorker != null)
- {
- return;
- }
if (InvokeRequired)
{
Invoke(new ProgressCallback(SetMaximumCrossThread), value);
@@ -171,10 +144,7 @@ private void SetMaximumCrossThread(int amount)
///
public int Progress
{
- get
- {
- return m_progressBar.Value;
- }
+ get => m_progressBar.Value;
set
{
/* these were causing weird, hard to debug (because of threads)
@@ -203,12 +173,10 @@ public int Progress
///
public bool CanCancel
{
- get
- {
- return m_cancelButton.Enabled || m_cancelLink.Enabled;
- }
+ get => m_cancellationTokenSource != null;
set
{
+ m_cancellationTokenSource = value ? new CancellationTokenSource() : null;
if (ReplaceCancelButtonWithLink)
{
m_cancelLink.Enabled = value;
@@ -222,70 +190,85 @@ public bool CanCancel
}
}
+ private CancellationToken CancellationToken => m_cancellationTokenSource?.Token ?? new CancellationToken(false);
+
///
- /// If this is set before showing, the dialog will run the worker and respond
- /// to its events
+ /// This should be an abstract, but abstract dialogs are hard to work with in Designer.
+ /// Override this in subclass to have the dialog kick off the work when shown.
///
- public BackgroundWorker BackgroundWorker
+ protected virtual void DoWork(CancellationToken cancellationToken)
{
- get
- {
- return m_backgroundWorker;
- }
- set
- {
- m_backgroundWorker = value;
- m_progressBar.Minimum = 0;
- m_progressBar.Maximum = 100;
- }
+
}
- public ProgressState ProgressStateResult
- {
- get
- {
- return m_progressState;
- }
- }
+ /////
+ ///// If this is set before showing, the dialog will run the worker and respond
+ ///// to its events
+ /////
+ //public BackgroundWorker BackgroundWorker
+ //{
+ // get
+ // {
+ // return m_backgroundWorker;
+ // }
+ // set
+ // {
+ // m_backgroundWorker = value;
+ // m_progressBar.Minimum = 0;
+ // m_progressBar.Maximum = 100;
+ // }
+ //}
+
+ //public ProgressState ProgressStateResult
+ //{
+ // get
+ // {
+ // return m_progressState;
+ // }
+ //}
///
/// Gets or sets the manner in which progress should be indicated on the progress bar.
///
- public ProgressBarStyle BarStyle { get { return m_progressBar.Style; } set { m_progressBar.Style = value; } }
-
- ///
- /// Optional; one will be created (of some class or subclass) if you don't set it.
- /// E.g. dlg.ProgressState = new BackgroundWorkerState(dlg.BackgroundWorker);
- /// Also, you can use the getter to gain access to the progressstate, in order to add arguments
- /// which the worker method can get at.
- ///
- public ProgressState ProgressState
+ public ProgressBarStyle BarStyle
{
- get
- {
- if(m_progressState ==null)
- {
- if(m_backgroundWorker == null)
- {
- throw new ArgumentException("You must set BackgroundWorker before accessing this property.");
- }
- ProgressState = new BackgroundWorkerState(m_backgroundWorker);
- }
- return m_progressState;
- }
-
- set
- {
- if (m_progressState!=null)
- {
- CancelRequested -= m_progressState.CancelRequested;
- }
- m_progressState = value;
- CancelRequested += m_progressState.CancelRequested;
- m_progressState.TotalNumberOfStepsChanged += OnTotalNumberOfStepsChanged;
- }
+ get => m_progressBar.Style;
+ set => m_progressBar.Style = value;
}
+ /////
+ ///// Optional; one will be created (of some class or subclass) if you don't set it.
+ ///// E.g. dlg.ProgressState = new BackgroundWorkerState(dlg.BackgroundWorker);
+ ///// Also, you can use the getter to gain access to the ProgressState, in order to add arguments
+ ///// which the worker method can get at.
+ /////
+ //public ProgressState ProgressState
+ //{
+ // get
+ // {
+ // if (m_progressState ==null)
+ // {
+ // if(m_backgroundWorker == null)
+ // {
+ // throw new ArgumentException("You must set BackgroundWorker before accessing this property.");
+ // }
+ // ProgressState = new BackgroundWorkerState(m_backgroundWorker);
+ // }
+ // return m_progressState;
+ // }
+
+ // set
+ // {
+ // if (m_progressState!=null)
+ // {
+ // CancelRequested -= m_progressState.CancelRequested;
+ // }
+ // m_progressState = value;
+ // CancelRequested += m_progressState.CancelRequested;
+ // m_progressState.TotalNumberOfStepsChanged += OnTotalNumberOfStepsChanged;
+ // }
+ //}
+
public string CancelLinkText
{
get => m_cancelLink.Text;
@@ -310,39 +293,7 @@ public bool ReplaceCancelButtonWithLink
}
}
- protected virtual void OnBackgroundWorker_RunWorkerCompleted(object sender, RunWorkerCompletedEventArgs e)
- {
- if(e.Cancelled || (ProgressStateResult != null && ProgressStateResult.Cancel))
- {
- DialogResult = DialogResult.Cancel;
- }
- else if (ProgressStateResult != null && (ProgressStateResult.State == ProgressState.StateValue.StoppedWithError
- || ProgressStateResult.ExceptionThatWasEncountered != null))
- {
- //this dialog really can't know whether this was an unexpected exception or not
- //so don't do this: Reporting.ErrorReporter.ReportException(ProgressStateResult.ExceptionThatWasEncountered, this, false);
- DialogResult = DialogResult.Abort;//not really matching semantics
- // _progressState.State = ProgressState.StateValue.StoppedWithError;
- }
- else
- {
- DialogResult = DialogResult.None;
- m_progressBar.Maximum = 1;
- m_progressBar.Value = 1;
- m_progressBar.Style = ProgressBarStyle.Blocks;
-
- m_progressLabel.Text = ProgressLabelTextWhenComplete;
-
- AcceptButton = m_okButton;
-
- m_okButton.Text = OkButtonText ?? LocalizationManager.GetString("Common.OK", "OK");
- m_okButton.DialogResult = DialogResult.OK;
- m_okButton.Enabled = true;
- m_okButton.Visible = true;
- }
- }
-
- void OnBackgroundWorker_ProgressChanged(object sender, ProgressChangedEventArgs e)
+ public void Report(ProgressChangedEventArgs e)
{
ProgressState state = e.UserState as ProgressState;
if (state != null)
@@ -350,8 +301,7 @@ void OnBackgroundWorker_ProgressChanged(object sender, ProgressChangedEventArgs
StatusText = state.StatusLabel;
}
- if (state == null
- || state is BackgroundWorkerState)
+ if (state == null || state is BackgroundWorkerState)
{
Progress = e.ProgressPercentage;
}
@@ -371,13 +321,11 @@ void OnBackgroundWorker_ProgressChanged(object sender, ProgressChangedEventArgs
/// Raises the cancelled event
///
/// Event data
- protected virtual void OnCancelled( EventArgs e )
+ private void OnCancelled( EventArgs e )
{
- EventHandler cancelled = CancelRequested;
- if( cancelled != null )
- {
- cancelled( this, e );
- }
+ DialogResult = DialogResult.Cancel;
+ m_cancellationTokenSource.Cancel();
+ CancelRequested?.Invoke(this, e);
}
///
@@ -385,14 +333,9 @@ protected virtual void OnCancelled( EventArgs e )
///
protected override void Dispose( bool disposing )
{
- if( disposing )
- {
- if (m_showWindowIfTakingLongTimeTimer != null)
- {
- m_showWindowIfTakingLongTimeTimer.Stop();
- }
- }
- base.Dispose( disposing );
+ if (disposing)
+ components.Dispose();
+ base.Dispose(disposing);
}
#region Windows Form Designer generated code
@@ -406,7 +349,6 @@ private void InitializeComponent()
this.m_statusLabel = new System.Windows.Forms.Label();
this.m_progressBar = new System.Windows.Forms.ProgressBar();
this.m_progressLabel = new System.Windows.Forms.Label();
- this.m_showWindowIfTakingLongTimeTimer = new System.Windows.Forms.Timer(this.components);
this.m_progressTimer = new System.Windows.Forms.Timer(this.components);
this.m_overviewLabel = new System.Windows.Forms.Label();
this.m_tableLayout = new System.Windows.Forms.TableLayoutPanel();
@@ -485,14 +427,8 @@ private void InitializeComponent()
this.m_progressLabel.UseMnemonic = false;
this.m_glyssenColorPalette.SetUsePaletteColors(this.m_progressLabel, true);
//
- // m_showWindowIfTakingLongTimeTimer
- //
- this.m_showWindowIfTakingLongTimeTimer.Interval = 2000;
- this.m_showWindowIfTakingLongTimeTimer.Tick += new System.EventHandler(this.OnTakingLongTimeTimerClick);
- //
// m_progressTimer
//
- this.m_progressTimer.Enabled = true;
this.m_progressTimer.Interval = 1000;
this.m_progressTimer.Tick += new System.EventHandler(this.progressTimer_Tick);
//
@@ -666,8 +602,6 @@ private void InitializeComponent()
this.StartPosition = System.Windows.Forms.FormStartPosition.CenterScreen;
this.Text = "Palaso";
this.m_glyssenColorPalette.SetUsePaletteColors(this, true);
- this.Load += new System.EventHandler(this.ProgressDialog_Load);
- this.Shown += new System.EventHandler(this.ProgressDialog_Shown);
this.m_tableLayout.ResumeLayout(false);
this.m_tableLayout.PerformLayout();
this.m_buttonPanel.ResumeLayout(false);
@@ -680,33 +614,14 @@ private void InitializeComponent()
}
#endregion
-
- private void OnTakingLongTimeTimerClick(object sender, EventArgs e)
- {
- // Show the window now the timer has elapsed, and stop the timer
- m_showWindowIfTakingLongTimeTimer.Stop();
- if (!Visible)
- {
- Show();
- }
- }
-
private void OnCancelButton_Click(object sender, EventArgs e)
{
- m_showWindowIfTakingLongTimeTimer.Stop();
-
- //Debug.WriteLine("Dialog:OnCancelButton_Click");
-
// Prevent further cancellation
m_cancelButton.Enabled = false;
m_progressTimer.Stop();
m_progressLabel.Text = LocalizationManager.GetString("DialogBoxes.ProgressDialogWithAcknowledgement.Canceling", "Canceling...");
// Tell people we're canceling
- OnCancelled( e );
- if (m_backgroundWorker != null && m_backgroundWorker.WorkerSupportsCancellation)
- {
- m_backgroundWorker.CancelAsync();
- }
+ OnCancelled(e);
}
private void progressTimer_Tick(object sender, EventArgs e)
@@ -744,90 +659,61 @@ private static string GetStringFor( TimeSpan span )
{
return string.Format(CultureInfo.CurrentUICulture, "{0} day {1} hour", span.Days, span.Hours);
}
- else if( span.TotalHours > 1 )
+ if( span.TotalHours > 1 )
{
return string.Format(CultureInfo.CurrentUICulture, "{0} hour {1} minutes", span.Hours, span.Minutes);
}
- else if( span.TotalMinutes > 1 )
+ if( span.TotalMinutes > 1 )
{
return string.Format(CultureInfo.CurrentUICulture, "{0} minutes {1} seconds", span.Minutes, span.Seconds);
}
return string.Format( CultureInfo.CurrentUICulture, "{0} seconds", span.Seconds );
}
- public void OnNumberOfStepsCompletedChanged(object sender, EventArgs e)
- {
- Progress = ((ProgressState) sender).NumberOfStepsCompleted;
- //in case there is no event pump showing us (mono-threaded)
- progressTimer_Tick(this, null);
- Refresh();
- }
-
- public void OnTotalNumberOfStepsChanged(object sender, EventArgs e)
- {
- if (InvokeRequired)
- {
- Invoke(new ProgressCallback(UpdateTotal), ((ProgressState)sender).TotalNumberOfSteps);
- }
- else
- {
- UpdateTotal(((ProgressState) sender).TotalNumberOfSteps);
- }
- }
-
- private void UpdateTotal(int steps)
- {
- m_startTime = DateTime.Now;
- ProgressRangeMaximum = steps;
- Refresh();
- }
-
- public void OnStatusLabelChanged(object sender, EventArgs e)
- {
- StatusText = ((ProgressState)sender).StatusLabel;
- Refresh();
- }
-
- private void OnStartWorker(object sender, EventArgs e)
+ private async Task OnStartWorker()
{
+ if (m_workerStarted)
+ return;
m_workerStarted = true;
- //Debug.WriteLine("Dialog:StartWorker");
+ m_progressTimer.Enabled = true;
- if (m_backgroundWorker != null)
+ try
{
- //BW uses percentages (unless it's using our custom ProgressState in the UserState member)
+ // Progress is reported as percentage
ProgressRangeMinimum = 0;
ProgressRangeMaximum = 100;
+ await Task.Run(() => { DoWork(CancellationToken); }, CancellationToken);
- //if the actual task can't take cancelling, the caller of this should set CanCancel to false;
- m_backgroundWorker.WorkerSupportsCancellation = CanCancel;
+ DialogResult = DialogResult.None;
+ m_progressBar.Maximum = 1;
+ m_progressBar.Value = 1;
+ m_progressBar.Style = ProgressBarStyle.Blocks;
- m_backgroundWorker.ProgressChanged += OnBackgroundWorker_ProgressChanged;
- m_backgroundWorker.RunWorkerCompleted += OnBackgroundWorker_RunWorkerCompleted;
- m_backgroundWorker.RunWorkerAsync(ProgressState);
- }
- }
+ m_progressLabel.Text = ProgressLabelTextWhenComplete;
- //This is here, in addition to the OnShown handler, because of a weird bug where a certain,
- //completely unrelated test (which doesn't use this class at all) can cause tests using this to
- //fail because the OnShown event is never fired.
- //I don't know why the orginal code we copied this from was using onshown instead of onload,
- //but it may have something to do with its "delay show" feature (which I couldn't get to work,
- //but which would be a terrific thing to have)
- private void ProgressDialog_Load(object sender, EventArgs e)
- {
- if(!m_workerStarted)
+ AcceptButton = m_okButton;
+
+ m_okButton.Text = OkButtonText ?? LocalizationManager.GetString("Common.OK", "OK");
+ m_okButton.DialogResult = DialogResult.OK;
+ m_okButton.Enabled = true;
+ m_okButton.Visible = true;
+ m_okButton.Visible = true;
+ }
+ catch (Exception exception)
{
- OnStartWorker(this, null);
+ Console.WriteLine(exception);
+ // This dialog really can't know whether this was an unexpected exception or not,
+ // so don't do this:
+ // Reporting.ErrorReporter.ReportException(ProgressStateResult.ExceptionThatWasEncountered, this, false);
+ DialogResult = DialogResult.Abort; //not really matching semantics
+ Close();
}
}
- private void ProgressDialog_Shown(object sender, EventArgs e)
+ protected override async void OnActivated(EventArgs e)
{
- if(!m_workerStarted)
- {
- OnStartWorker(this, null);
- }
+ base.OnActivated(e);
+ await OnStartWorker();
}
private void OnCancelLink_LinkClicked(object sender, LinkLabelLinkClickedEventArgs e)
diff --git a/Glyssen/MainForm.cs b/Glyssen/MainForm.cs
index 32d3b9cbf..12cae261d 100644
--- a/Glyssen/MainForm.cs
+++ b/Glyssen/MainForm.cs
@@ -1,6 +1,5 @@
using System;
using System.Collections.Generic;
-using System.ComponentModel;
using System.Diagnostics;
using System.Drawing;
using System.Globalization;
@@ -26,7 +25,6 @@
using L10NSharp.UI;
using SIL.DblBundle;
using SIL.IO;
-using SIL.Progress;
using SIL.Reporting;
using SIL.Windows.Forms;
using SIL.Windows.Forms.Miscellaneous;
@@ -185,6 +183,14 @@ private void SetProject(Project project)
}
private void FinishSetProjectIfReady(object sender, EventArgs e)
+ {
+ if (InvokeRequired)
+ Invoke(new Action(FinishSetProjectIfReady));
+ else
+ FinishSetProjectIfReady();
+ }
+
+ private void FinishSetProjectIfReady()
{
if (m_project != null && (m_project.ProjectState & ProjectState.ReadyForUserInteraction) > 0)
FinishSetProject();
@@ -890,11 +896,9 @@ private void EnsureGroupsAreInSynchWithCharactersInUse()
var adjuster = new CharacterGroupsAdjuster(m_project);
if (adjuster.GroupsAreNotInSynchWithData)
{
- using (var progressDialog = new GenerateGroupsProgressDialog(m_project, OnGenerateGroupsWorkerDoWork, false, true))
+ var generator = new CharacterGroupGenerator(m_project, ProjectCastSizePlanningViewModel.SelectedCastSize);
+ using (var progressDialog = new GenerateGroupsProgressDialog(m_project, generator, false, true))
{
- var generator = new CharacterGroupGenerator(m_project, ProjectCastSizePlanningViewModel.SelectedCastSize, progressDialog.BackgroundWorker);
- progressDialog.ProgressState.Arguments = generator;
-
if (progressDialog.ShowDialog() == DialogResult.OK && generator.GeneratedGroups != null)
{
var assignedBefore = m_project.CharacterGroupList.CountVoiceActorsAssigned();
@@ -924,12 +928,6 @@ private void EnsureGroupsAreInSynchWithCharactersInUse()
CharacterGroupList.AssignGroupIds(m_project.CharacterGroupList.CharacterGroups);
}
- private void OnGenerateGroupsWorkerDoWork(object s, DoWorkEventArgs e)
- {
- var generator = (CharacterGroupGenerator)((ProgressState)e.Argument).Arguments;
- generator.GenerateCharacterGroups();
- }
-
private bool IsOkToExport(ProjectExporter exporter)
{
var export = true;
diff --git a/GlyssenEngine/Project.cs b/GlyssenEngine/Project.cs
index 942e56af2..e43d26045 100644
--- a/GlyssenEngine/Project.cs
+++ b/GlyssenEngine/Project.cs
@@ -193,7 +193,7 @@ public Project(GlyssenBundle bundle, string recordingProjectName = null, Project
PopulateAndParseBooks(bundle);
}
- public Project(ParatextScrTextWrapper paratextProject) :
+ public Project(ParatextScrTextWrapper paratextProject, Action exceptionHandler = null) :
this(paratextProject.GlyssenDblTextMetadata, null, false, paratextProject.WritingSystem)
{
Writer.SetUpProjectPersistence(this);
@@ -203,7 +203,7 @@ public Project(ParatextScrTextWrapper paratextProject) :
SetWsQuotationMarksUsingFullySpecifiedContinuers(paratextProject.QuotationMarks);
}
- ParseAndSetBooks(paratextProject.UsxDocumentsForIncludedBooks, paratextProject.Stylesheet);
+ ParseAndSetBooks(paratextProject.UsxDocumentsForIncludedBooks, paratextProject.Stylesheet, exceptionHandler);
}
///
@@ -337,7 +337,7 @@ public void SetQuoteSystem(QuoteSystemStatus status, QuoteSystem system)
if (IsQuoteSystemReadyForParse && ProjectState == ProjectState.NeedsQuoteSystemConfirmation)
{
m_quoteSystem = system;
- DoQuoteParse();
+ DoQuoteParseAsync();
}
else if ((quoteSystemChanged && !quoteSystemBeingSetForFirstTime) ||
(QuoteSystemStatus == QuoteSystemStatus.Reviewed &&
@@ -1379,7 +1379,7 @@ private void InitializeLoadedProject()
m_usxPercentComplete = 100;
if (QuoteSystem == null)
{
- GuessAtQuoteSystem();
+ GuessAtQuoteSystemAsync();
UpdateControlFileVersion();
return;
}
@@ -1478,7 +1478,7 @@ public void IncludeExistingBook(BookScript book)
m_books.Insert(i, book);
}
- public void IncludeBooksFromParatext(ParatextScrTextWrapper wrapper, ISet bookNumbers, Action postParseAction)
+ public async Task IncludeBooksFromParatext(ParatextScrTextWrapper wrapper, ISet bookNumbers, Action postParseAction)
{
wrapper.IncludeBooks(bookNumbers.Select(BCVRef.NumberToBookCode));
var usxBookInfoList = wrapper.GetUsxDocumentsForIncludedParatextBooks(bookNumbers);
@@ -1491,65 +1491,72 @@ void EnhancedPostParseAction(BookScript book)
postParseAction?.Invoke(book);
}
- ParseAndIncludeBooks(usxBookInfoList, wrapper.Stylesheet, EnhancedPostParseAction);
+ await ParseAndIncludeBooks(usxBookInfoList, wrapper.Stylesheet, null, EnhancedPostParseAction);
}
- private void ParseAndSetBooks(IEnumerable books, IStylesheet stylesheet)
+ private async Task ParseAndSetBooks(IEnumerable books, IStylesheet stylesheet, Action exceptionHandler = null)
{
if (m_books.Any())
throw new InvalidOperationException("Project already contains books. If the intention is to replace the existing ones, let's clear the list first. Otherwise, call ParseAndIncludeBooks.");
- ParseAndIncludeBooks(books, stylesheet);
+ await ParseAndIncludeBooks(books, stylesheet, exceptionHandler);
}
- private void ParseAndIncludeBooks(IEnumerable books, IStylesheet stylesheet, Action postParseAction = null)
+ private async Task ParseAndIncludeBooks(IEnumerable books, IStylesheet stylesheet,
+ Action exceptionHandler/* = null*/, Action postParseAction = null)
{
if (Versification == null)
throw new NullReferenceException("What!!!");
ProjectState = ProjectState.Initial | (ProjectState & ProjectState.WritingSystemRecoveryInProcess);
- var usxWorker = new BackgroundWorker {WorkerReportsProgress = true};
- usxWorker.DoWork += UsxWorker_DoWork;
- usxWorker.RunWorkerCompleted += UsxWorker_RunWorkerCompleted;
- usxWorker.ProgressChanged += UsxWorker_ProgressChanged;
+ List parsedBooks = null;
+ try
+ {
+ await Task.Run(() =>
+ {
+ parsedBooks = UsxParse(books, stylesheet, postParseAction);
+ });
- object[] parameters = {books, stylesheet, postParseAction};
- usxWorker.RunWorkerAsync(parameters);
+ await Task.Run(() =>
+ {
+ if (QuoteSystem == null)
+ GuessAtQuoteSystem();
+ else if (IsQuoteSystemReadyForParse)
+ DoQuoteParse(parsedBooks.Select(b => b.BookId));
+ });
+ }
+ catch (Exception e)
+ {
+ Console.WriteLine(e);
+ if (exceptionHandler == null)
+ ErrorReport.ReportFatalException(e);
+ else
+ exceptionHandler(e);
+ }
}
- private void UsxWorker_DoWork(object sender, DoWorkEventArgs e)
+ private List UsxParse(IEnumerable books, IStylesheet stylesheet, Action postParseAction = null)
{
- var parameters = (object[])e.Argument;
- var books = (IEnumerable)parameters[0];
- var stylesheet = (IStylesheet)parameters[1];
- var postParseAction = parameters.Length > 2 ? (Action)parameters[2] : null;
-
- var backgroundWorker = (BackgroundWorker)sender;
-
- var parsedBooks = UsxParser.ParseBooks(books, stylesheet, i => backgroundWorker.ReportProgress(i));
+ var parsedBooks = UsxParser.ParseBooks(books, stylesheet, i =>
+ {
+ m_usxPercentComplete = i;
+ var pe = new ProgressChangedEventArgs(PercentInitialized, null);
+ OnReport(pe);
+ });
if (postParseAction != null)
{
foreach (var book in parsedBooks)
postParseAction(book);
}
- e.Result = parsedBooks;
- }
-
- private void UsxWorker_RunWorkerCompleted(object sender, RunWorkerCompletedEventArgs e)
- {
- if (e.Error != null)
- throw e.Error;
- var bookScripts = (List)e.Result;
-
- foreach (var bookScript in bookScripts)
+ foreach (var bookScript in parsedBooks)
{
// This code is an attempt to figure out how we are getting null reference exceptions when using the objects in the list (See PG-275 & PG-287)
if (bookScript?.BookId == null)
{
- var nonNullBookScripts = bookScripts.Where(b => b != null).Select(b => b.BookId);
+ var nonNullBookScripts = parsedBooks.Where(b => b != null).Select(b => b.BookId);
var nonNullBookScriptsStr = Join(";", nonNullBookScripts);
var initialMessage = bookScript == null ? "BookScript is null." : "BookScript has null BookId.";
- throw new ApplicationException($"{initialMessage} Number of BookScripts: {bookScripts.Count}. " +
+ throw new ApplicationException($"{initialMessage} Number of BookScripts: {parsedBooks.Count}. " +
$"BookScripts which are NOT null: {nonNullBookScriptsStr}");
}
@@ -1560,12 +1567,12 @@ private void UsxWorker_RunWorkerCompleted(object sender, RunWorkerCompletedEvent
if (m_books.Any())
{
- foreach (var book in bookScripts)
+ foreach (var book in parsedBooks)
IncludeExistingBook(book);
}
else
{
- m_books.AddRange(bookScripts);
+ m_books.AddRange(parsedBooks);
m_projectMetadata.ParserVersion = kParserVersion;
if (m_books.All(b => IsNullOrEmpty(b.PageHeader)))
ChapterAnnouncementStyle = ChapterAnnouncement.ChapterLabel;
@@ -1574,80 +1581,55 @@ private void UsxWorker_RunWorkerCompleted(object sender, RunWorkerCompletedEvent
AddMissingAvailableBooks();
}
- if (QuoteSystem == null)
- GuessAtQuoteSystem();
- else if (IsQuoteSystemReadyForParse)
- DoQuoteParse(bookScripts.Select(b => b.BookId));
+ return parsedBooks;
}
- private void UsxWorker_ProgressChanged(object sender, ProgressChangedEventArgs e)
+ private async Task GuessAtQuoteSystemAsync()
{
- m_usxPercentComplete = e.ProgressPercentage;
- var pe = new ProgressChangedEventArgs(PercentInitialized, null);
- OnReport(pe);
+ await Task.Run(GuessAtQuoteSystem);
}
private void GuessAtQuoteSystem()
{
ProjectState = ProjectState.UsxComplete | (ProjectState & ProjectState.WritingSystemRecoveryInProcess);
- var guessWorker = new BackgroundWorker {WorkerReportsProgress = true};
- guessWorker.DoWork += GuessWorker_DoWork;
- guessWorker.RunWorkerCompleted += GuessWorker_RunWorkerCompleted;
- guessWorker.ProgressChanged += GuessWorker_ProgressChanged;
- guessWorker.RunWorkerAsync();
- }
-
- private void GuessWorker_DoWork(object sender, DoWorkEventArgs e)
- {
- e.Result = QuoteSystemGuesser.Guess(ControlCharacterVerseData.Singleton, m_books, Versification, out _,
- sender as BackgroundWorker);
- }
-
- private void GuessWorker_RunWorkerCompleted(object sender, RunWorkerCompletedEventArgs e)
- {
- if (e.Error != null)
- throw e.Error;
-
- SetQuoteSystem(QuoteSystemStatus.Guessed, (QuoteSystem)e.Result);
+ var quoteSystem = QuoteSystemGuesser.Guess(ControlCharacterVerseData.Singleton, m_books, Versification, out _,
+ i =>
+ {
+ m_guessPercentComplete = i;
+ var pe = new ProgressChangedEventArgs(PercentInitialized, null);
+ OnReport(pe);
+ });
+ SetQuoteSystem(QuoteSystemStatus.Guessed, quoteSystem);
Save();
}
- private void GuessWorker_ProgressChanged(object sender, ProgressChangedEventArgs e)
+ private async Task DoQuoteParseAsync(IEnumerable booksToParse = null)
{
- m_guessPercentComplete = e.ProgressPercentage;
- var pe = new ProgressChangedEventArgs(PercentInitialized, null);
- OnReport(pe);
+ await Task.Run(() => { DoQuoteParse(booksToParse);});
}
- private void DoQuoteParse(IEnumerable booksToParse = null)
+ private void DoQuoteParse(IEnumerable bookIds)
{
m_projectMetadata.ParserVersion = kParserVersion;
ProjectState = ProjectState.Parsing;
- var quoteWorker = new BackgroundWorker {WorkerReportsProgress = true};
- quoteWorker.DoWork += QuoteWorker_DoWork;
- quoteWorker.RunWorkerCompleted += QuoteWorker_RunWorkerCompleted;
- quoteWorker.ProgressChanged += QuoteWorker_ProgressChanged;
- object[] parameters = {booksToParse};
- quoteWorker.RunWorkerAsync(parameters);
- }
-
- private void QuoteWorker_DoWork(object sender, DoWorkEventArgs e)
- {
- var bookIds = (IEnumerable)((object[])e.Argument)[0];
- QuoteParser.ParseProject(this, sender as BackgroundWorker, bookIds);
- }
-
- private void QuoteWorker_RunWorkerCompleted(object sender, RunWorkerCompletedEventArgs e)
- {
- if (e.Error != null)
+ try
+ {
+ QuoteParser.ParseProject(this, i =>
+ {
+ m_quotePercentComplete = i;
+ var pe = new ProgressChangedEventArgs(PercentInitialized, null);
+ OnReport(pe);
+ }, bookIds);
+ }
+ catch (Exception e)
{
#if DEBUG
Exception innerException;
- if ((innerException = e.Error?.InnerException) != null)
+ if ((innerException = e.InnerException) != null)
Debug.WriteLine(innerException.Message + innerException.StackTrace);
#endif
- throw e.Error;
+ throw;
}
ProjectState = ProjectState.QuoteParseComplete;
@@ -1668,13 +1650,6 @@ private void QuoteWorker_RunWorkerCompleted(object sender, RunWorkerCompletedEve
QuoteParseCompleted?.Invoke(this, new EventArgs());
}
- private void QuoteWorker_ProgressChanged(object sender, ProgressChangedEventArgs e)
- {
- m_quotePercentComplete = e.ProgressPercentage;
- var pe = new ProgressChangedEventArgs(PercentInitialized, null);
- OnReport(pe);
- }
-
public int MaxProjectNameLength => Writer.GetMaxProjectNameLength(this);
public BookScript LoadExistingBookIfPossible(string bookId)
diff --git a/GlyssenEngine/Quote/QuoteParser.cs b/GlyssenEngine/Quote/QuoteParser.cs
index 665b40686..67605cf08 100644
--- a/GlyssenEngine/Quote/QuoteParser.cs
+++ b/GlyssenEngine/Quote/QuoteParser.cs
@@ -1,7 +1,6 @@
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
-using System.ComponentModel;
using System.Diagnostics;
using System.Globalization;
using System.Linq;
@@ -21,7 +20,7 @@ namespace GlyssenEngine.Quote
{
public class QuoteParser
{
- public static void ParseProject(Project project, BackgroundWorker projectWorker, IEnumerable bookIdsToParse)
+ public static void ParseProject(Project project, Action reportProgress, IEnumerable bookIdsToParse)
{
var cvInfo = new ParserCharacterRepository(new CombinedCharacterVerseData(project), project.ReferenceText);
@@ -43,10 +42,10 @@ public static void ParseProject(Project project, BackgroundWorker projectWorker,
{
book.Blocks = new QuoteParser(cvInfo, book.BookId, blocksInBook[book], project.Versification).Parse().ToList();
completedProjectBlocks += numBlocksPerBook[book.BookId];
- projectWorker.ReportProgress(MathUtilities.Percent(completedProjectBlocks, allProjectBlocks, 99));
+ reportProgress(MathUtilities.Percent(completedProjectBlocks, allProjectBlocks, 99));
});
- projectWorker.ReportProgress(100);
+ reportProgress(100);
}
public static void SetQuoteSystem(QuoteSystem quoteSystem)
diff --git a/GlyssenEngine/Quote/QuoteSystemGuesser.cs b/GlyssenEngine/Quote/QuoteSystemGuesser.cs
index e70e7e9d3..f5576260e 100644
--- a/GlyssenEngine/Quote/QuoteSystemGuesser.cs
+++ b/GlyssenEngine/Quote/QuoteSystemGuesser.cs
@@ -2,7 +2,6 @@
using System;
using System.Collections.Generic;
-using System.ComponentModel;
using System.Diagnostics;
using System.Linq;
using Glyssen.Shared;
@@ -37,13 +36,13 @@ private static void IncrementScore(Dictionary scores, QuoteSys
bestScore = scores[quoteSystem];
}
- public static QuoteSystem Guess(ICharacterVerseInfo cvInfo, List bookList, ScrVers versification, out bool certain, BackgroundWorker worker = null) where T : IScrBook
+ public static QuoteSystem Guess(ICharacterVerseInfo cvInfo, List bookList, ScrVers versification, out bool certain, Action reportProgress = null) where T : IScrBook
{
certain = false;
var bookCount = bookList.Count;
if (bookCount == 0)
{
- ReportProgressComplete(worker);
+ reportProgress?.Invoke(100);
return QuoteSystem.Default;
}
var scores = QuoteSystem.UniquelyGuessableSystems.ToDictionary(s => s, s => 0);
@@ -73,8 +72,7 @@ public static QuoteSystem Guess(ICharacterVerseInfo cvInfo, List bookList,
var bookNum = bookTuple.Item1;
var book = bookTuple.Item2;
- if (worker != null)
- worker.ReportProgress(MathUtilities.Percent(++booksProcessed, bookCount));
+ reportProgress?.Invoke(MathUtilities.Percent(++booksProcessed, bookCount));
int versesAnalyzedForCurrentBook = 0;
int prevQuoteChapter = -1;
@@ -209,7 +207,7 @@ public static QuoteSystem Guess(ICharacterVerseInfo cvInfo, List bookList,
if (competitors.Count == 1)
{
certain = true;
- ReportProgressComplete(worker);
+ reportProgress?.Invoke(100);
return competitors[0];
}
@@ -297,7 +295,7 @@ public static QuoteSystem Guess(ICharacterVerseInfo cvInfo, List bookList,
if (competitors.Any())
{
- ReportProgressComplete(worker);
+ reportProgress?.Invoke(100);
if (competitors.Count == 1)
return competitors[0];
@@ -327,7 +325,7 @@ public static QuoteSystem Guess(ICharacterVerseInfo cvInfo, List bookList,
#if SHOWTESTINFO
Debug.WriteLine("Time-out guessing quote system.");
#endif
- ReportProgressComplete(worker);
+ reportProgress?.Invoke(100);
return BestGuess(viableSystems, scores, bestScore, foundEndQuote);
}
@@ -335,7 +333,7 @@ public static QuoteSystem Guess(ICharacterVerseInfo cvInfo, List bookList,
prevQuoteVerse = quote.Verse;
}
}
- ReportProgressComplete(worker);
+ reportProgress?.Invoke(100);
return BestGuess(viableSystems, scores, bestScore, foundEndQuote);
}
@@ -427,11 +425,5 @@ private static QuoteSystem BestGuess(IEnumerable viableSystems, Dic
// newSystem.AllLevels.Add(QuoteUtils.GenerateLevel3(newSystem, true));
// return newSystem;
//}
-
- private static void ReportProgressComplete(BackgroundWorker worker)
- {
- if (worker != null)
- worker.ReportProgress(100);
- }
}
}
diff --git a/GlyssenEngine/Rules/CharacterGroupGenerator.cs b/GlyssenEngine/Rules/CharacterGroupGenerator.cs
index 17073d561..eff7b5b42 100644
--- a/GlyssenEngine/Rules/CharacterGroupGenerator.cs
+++ b/GlyssenEngine/Rules/CharacterGroupGenerator.cs
@@ -1,8 +1,8 @@
using System;
using System.Collections.Generic;
-using System.ComponentModel;
using System.Diagnostics;
using System.Linq;
+using System.Threading;
using GlyssenEngine.Bundle;
using GlyssenEngine.Casting;
using GlyssenEngine.Character;
@@ -18,7 +18,7 @@ public class CharacterGroupGenerator
private readonly Project m_project;
private readonly CastSizeRowValues m_ghostCastSize;
private readonly Proximity m_proximity;
- private readonly BackgroundWorker m_worker;
+ private CancellationToken m_cancellationToken;
private static readonly SortedDictionary>> s_deityCharacters;
@@ -45,12 +45,11 @@ static CharacterGroupGenerator()
s_deityCharacters.Add(17, new List> { jesusSet, new HashSet { "God" }, holySpiritSet, new HashSet { CharacterVerse.kScriptureCharacter } });
}
- public CharacterGroupGenerator(Project project, CastSizeRowValues ghostCastSize = null, BackgroundWorker worker = null)
+ public CharacterGroupGenerator(Project project, CastSizeRowValues ghostCastSize = null)
{
m_project = project;
m_ghostCastSize = ghostCastSize;
m_proximity = new Proximity(project);
- m_worker = worker ?? new BackgroundWorker();
}
public List GeneratedGroups { get; private set; }
@@ -105,6 +104,13 @@ private static void LogAndOutputToDebugConsole(string message)
public List GenerateCharacterGroups(bool enforceProximityAndGenderConstraints = false)
{
+ return GenerateCharacterGroups(new CancellationToken(false), enforceProximityAndGenderConstraints);
+ }
+
+ public List GenerateCharacterGroups(CancellationToken cancellationToken, bool enforceProximityAndGenderConstraints = false)
+ {
+ m_cancellationToken = cancellationToken;
+
m_project.SetDefaultCharacterGroupGenerationPreferences();
List actorsForGeneration;
@@ -119,7 +125,7 @@ public List GenerateCharacterGroups(bool enforceProximityAndGend
}
List characterGroups = CreateGroupsForActors(actorsForGeneration);
- if (m_worker.CancellationPending)
+ if (m_cancellationToken.IsCancellationRequested)
{
EnsureActorListIsSetToRealActors(realActorsToReset);
return GeneratedGroups = null;
@@ -133,7 +139,7 @@ public List GenerateCharacterGroups(bool enforceProximityAndGend
List nonCameoActors = actorsForGeneration.Where(a => !a.IsCameo).ToList();
- if (m_worker.CancellationPending)
+ if (m_cancellationToken.IsCancellationRequested)
{
EnsureActorListIsSetToRealActors(realActorsToReset);
return GeneratedGroups = null;
@@ -151,7 +157,7 @@ public List GenerateCharacterGroups(bool enforceProximityAndGend
IReadOnlyDictionary characterDetails = m_project.AllCharacterDetailDictionary;
var includedCharacterDetails = characterDetails.Values.Where(c => characterIdsOrderedToMinimizeProximityConflicts.Select(e => e.Key).Contains(c.CharacterId)).ToList();
- if (m_worker.CancellationPending)
+ if (m_cancellationToken.IsCancellationRequested)
{
EnsureActorListIsSetToRealActors(realActorsToReset);
return GeneratedGroups = null;
@@ -194,7 +200,7 @@ public List GenerateCharacterGroups(bool enforceProximityAndGend
}
}
- if (m_worker.CancellationPending)
+ if (m_cancellationToken.IsCancellationRequested)
{
EnsureActorListIsSetToRealActors(realActorsToReset);
return GeneratedGroups = null;
@@ -227,7 +233,7 @@ public List GenerateCharacterGroups(bool enforceProximityAndGend
Debug.WriteLine("===========================================================");
}
- if (m_worker.CancellationPending)
+ if (m_cancellationToken.IsCancellationRequested)
{
EnsureActorListIsSetToRealActors(realActorsToReset);
return GeneratedGroups = null;
@@ -250,7 +256,7 @@ public List GenerateCharacterGroups(bool enforceProximityAndGend
{
foreach (var configuration in trialConfigurationsForNarratorsAndExtras)
{
- if (m_worker.CancellationPending)
+ if (m_cancellationToken.IsCancellationRequested)
{
EnsureActorListIsSetToRealActors(realActorsToReset);
return GeneratedGroups = null;
@@ -360,7 +366,7 @@ private List GetFinalizedGroups(TrialGroupConfiguration configur
{
List groups = configuration.Groups;
- if (m_worker.CancellationPending)
+ if (m_cancellationToken.IsCancellationRequested)
{
EnsureActorListIsSetToRealActors(realActorsToReset);
return GeneratedGroups = null;
diff --git a/GlyssenEngineTests/Rules/CharacterGroupGeneratorTests.cs b/GlyssenEngineTests/Rules/CharacterGroupGeneratorTests.cs
index 5aea1d304..b4b849092 100644
--- a/GlyssenEngineTests/Rules/CharacterGroupGeneratorTests.cs
+++ b/GlyssenEngineTests/Rules/CharacterGroupGeneratorTests.cs
@@ -4,6 +4,7 @@
using System.Diagnostics;
using System.Linq;
using System.Threading;
+using System.Threading.Tasks;
using GlyssenEngine;
using GlyssenEngine.Bundle;
using GlyssenEngine.Casting;
@@ -1477,7 +1478,7 @@ public void SetUp()
}
[Test]
- public void GenerateCharacterGroups_IsCancelable()
+ public async Task GenerateCharacterGroups_Cancel_GenerationCancelsLeavingOriginalGroups()
{
SetVoiceActors(10);
m_testProject.CharacterGroupList.CharacterGroups.Clear();
@@ -1485,22 +1486,10 @@ public void GenerateCharacterGroups_IsCancelable()
group.AssignVoiceActor(m_testProject.VoiceActorList.AllActors[0].Id);
m_testProject.CharacterGroupList.CharacterGroups.Add(group);
- BackgroundWorker worker = new BackgroundWorker {WorkerSupportsCancellation = true};
- CharacterGroupGenerator generator = new CharacterGroupGenerator(m_testProject, null, worker);
- worker.DoWork += (sender, args) =>
- {
- generator.GenerateCharacterGroups();
- };
-
- var start = DateTime.Now;
- worker.RunWorkerAsync();
- worker.CancelAsync();
-
- while (worker.IsBusy)
- {
- Assert.IsTrue(DateTime.Now.Subtract(start).Seconds < 6, "Failed to cancel within timeout (6 seconds)");
- Thread.Sleep(100);
- }
+ CharacterGroupGenerator generator = new CharacterGroupGenerator(m_testProject);
+ var source = new CancellationTokenSource();
+ source.CancelAfter(TimeSpan.FromMilliseconds(200));
+ await Task.Run(() => { generator.GenerateCharacterGroups(source.Token); }, source.Token);
Assert.Null(generator.GeneratedGroups);
Assert.AreEqual(1, m_testProject.CharacterGroupList.CharacterGroups.Count);