22using System . Collections . Generic ;
33using System . ComponentModel ;
44using System . ComponentModel . Design ;
5+ using System . Diagnostics ;
56using System . Drawing ;
67using System . Linq ;
78using System . Media ;
1011using L10NSharp . UI ;
1112using SIL . Code ;
1213using SIL . Core . ClearShare ;
14+ using SIL . Reporting ;
1315using SIL . Windows . Forms . Extensions ;
1416using SIL . Windows . Forms . Widgets . BetterGrid ;
17+ using static System . String ;
1518
1619namespace SIL . Windows . Forms . ClearShare . WinFormsUI
1720{
@@ -39,7 +42,7 @@ public delegate KeyValuePair<string, string> ValidatingContributorHandler(
3942 public event ColumnHeaderMouseClickHandler ColumnHeaderMouseClick ;
4043
4144 private FadingMessageWindow _msgWindow ;
42- private readonly ContributorsListControlViewModel _model ;
45+ private ContributorsListControlViewModel _model ;
4346
4447 /// ------------------------------------------------------------------------------------
4548 public ContributorsListControl ( )
@@ -48,6 +51,13 @@ public ContributorsListControl()
4851 _grid . DataError += _grid_DataError ;
4952 }
5053
54+ /// ------------------------------------------------------------------------------------
55+ public ContributorsListControl ( ContributorsListControlViewModel model ) : this ( )
56+ {
57+ Initialize ( model ) ;
58+ }
59+
60+ /// ------------------------------------------------------------------------------------
5161 private void _grid_DataError ( object sender , DataGridViewDataErrorEventArgs e )
5262 {
5363 if ( e . Exception != null &&
@@ -58,11 +68,25 @@ private void _grid_DataError(object sender, DataGridViewDataErrorEventArgs e)
5868 }
5969
6070 /// ------------------------------------------------------------------------------------
61- public ContributorsListControl ( ContributorsListControlViewModel model ) : this ( )
71+ [ PublicAPI ]
72+ public void Initialize ( ContributorsListControlViewModel model )
6273 {
74+ Debug . Assert ( _model == null ) ;
75+
6376 _model = model ;
6477 _model . NewContributionListAvailable += HandleNewContributionListAvailable ;
6578
79+ // Do not use SafeInvoke here because we want this to work even if called before the
80+ // handle is created.
81+ if ( InvokeRequired )
82+ Invoke ( new Action ( InitializeGrid ) ) ;
83+ else
84+ InitializeGrid ( ) ;
85+ }
86+
87+ /// ------------------------------------------------------------------------------------
88+ private void InitializeGrid ( )
89+ {
6690 _grid . Font = SystemFonts . MenuFont ;
6791
6892 DataGridViewColumn col = BetterGrid . CreateTextBoxColumn ( kNameColName , "Name" ) ;
@@ -99,8 +123,12 @@ public ContributorsListControl(ContributorsListControlViewModel model) : this()
99123 _grid . ColumnHeaderMouseClick += _grid_ColumnHeaderMouseClick ;
100124
101125 _model . ContributorsGridSettings ? . InitializeGrid ( _grid ) ;
126+
127+ if ( _model . Contributions . Any ( ) )
128+ HandleNewContributionListAvailable ( _model , EventArgs . Empty ) ;
102129 }
103130
131+ /// ------------------------------------------------------------------------------------
104132 // SP-874: Not able to open L10NSharp with Alt-Shift-click
105133 private void _grid_ColumnHeaderMouseClick ( object sender , DataGridViewCellMouseEventArgs e )
106134 {
@@ -119,26 +147,37 @@ private void _grid_ColumnHeaderMouseClick(object sender, DataGridViewCellMouseEv
119147 GetService ( typeof ( IDesignerHost ) ) != null ||
120148 LicenseManager . UsageMode == LicenseUsageMode . Designtime ;
121149
150+ /// ------------------------------------------------------------------------------------
122151 public void SetColumnAutoSizeMode ( StandardColumns col , DataGridViewAutoSizeColumnMode autoSizeMode )
123152 {
124- DataGridViewColumn column ;
125- switch ( col )
153+ if ( _model == null )
126154 {
127- case StandardColumns . Name :
128- column = Grid . Columns [ kNameColName ] ;
129- break ;
130- case StandardColumns . Role :
131- column = Grid . Columns [ kRoleColName ] ;
132- break ;
133- case StandardColumns . Comments :
134- column = Grid . Columns [ kCommentsColName ] ;
135- break ;
136- case StandardColumns . Date :
137- column = Grid . Columns [ kDateColName ] ;
138- break ;
139- default :
140- throw new ArgumentOutOfRangeException ( nameof ( col ) , col , null ) ;
155+ if ( IsDisposed )
156+ throw new ObjectDisposedException ( Name ?? GetType ( ) . Name ) ;
157+ throw new InvalidOperationException (
158+ $ "{ nameof ( Initialize ) } must be called to set the model first.") ;
141159 }
160+
161+ // Do not use SafeInvoke here because we want this to work even if called before the
162+ // handle is created.
163+ if ( Grid . InvokeRequired )
164+ Grid . Invoke ( new Action ( ( ) => SetColumnAutoSizeMode_Internal ( col , autoSizeMode ) ) ) ;
165+ else
166+ SetColumnAutoSizeMode_Internal ( col , autoSizeMode ) ;
167+ }
168+
169+ /// ------------------------------------------------------------------------------------
170+ private void SetColumnAutoSizeMode_Internal ( StandardColumns col , DataGridViewAutoSizeColumnMode autoSizeMode )
171+ {
172+ var column = col switch
173+ {
174+ StandardColumns . Name => Grid . Columns [ kNameColName ] ,
175+ StandardColumns . Role => Grid . Columns [ kRoleColName ] ,
176+ StandardColumns . Comments => Grid . Columns [ kCommentsColName ] ,
177+ StandardColumns . Date => Grid . Columns [ kDateColName ] ,
178+ _ => throw new ArgumentOutOfRangeException ( nameof ( col ) , col , null )
179+ } ;
180+
142181 if ( column != null )
143182 column . AutoSizeMode = autoSizeMode ;
144183 }
@@ -149,7 +188,7 @@ public bool InEditMode
149188 {
150189 get
151190 {
152- if ( _grid . InvokeRequired )
191+ if ( InvokeRequired )
153192 return ( bool ) _grid . Invoke ( new Func < bool > ( ( ) => _grid . IsCurrentRowDirty ) ) ;
154193 return _grid . IsCurrentRowDirty ;
155194 }
@@ -175,23 +214,50 @@ protected override void OnHandleDestroyed(EventArgs e)
175214 base . OnHandleDestroyed ( e ) ;
176215 }
177216
178- /// ------------------------------------------------------------------------------------
179217 private void HandleNewContributionListAvailable ( object sender , EventArgs e )
180218 {
181- Guard . AgainstNull ( _model . Contributions , "Contributions" ) ;
219+ if ( InvokeRequired )
220+ {
221+ // Since BeginInvoke ensures the call is executed in the UI message loop (at a
222+ // later time), it might avoid conflicts with UI state updates still in progress.
223+ _grid . BeginInvoke ( new Action ( ( ) => HandleNewContributionListAvailable ( sender , e ) ) ) ;
224+ return ;
225+ }
182226
183- _grid . RowValidated -= HandleGridRowValidated ;
184- _grid . RowsRemoved -= HandleGridRowsRemoved ;
185- _grid . Rows . Clear ( ) ;
227+ Guard . AgainstNull ( _model . Contributions , nameof ( _model . Contributions ) ) ;
186228
187- foreach ( var contribution in _model . Contributions )
188- _grid . Rows . Add ( contribution . ContributorName , contribution . Role . Name , contribution . Date , contribution . Comments ) ;
229+ try
230+ {
231+ // Ensure any pending edits are committed or canceled before modifying rows
232+ if ( _grid . IsCurrentCellInEditMode )
233+ _grid . EndEdit ( ) ;
234+ if ( _grid . CurrentCell != null && _grid . CurrentRow ? . IsNewRow == false )
235+ _grid . CommitEdit ( DataGridViewDataErrorContexts . Commit ) ;
189236
190- _grid . CurrentCell = _grid [ 0 , 0 ] ;
191- _grid . IsDirty = false ;
237+ _grid . RowValidated -= HandleGridRowValidated ;
238+ _grid . RowsRemoved -= HandleGridRowsRemoved ;
239+
240+ _grid . SuspendLayout ( ) ; // Improve performance when modifying multiple rows
241+ _grid . Rows . Clear ( ) ;
192242
193- _grid . RowValidated += HandleGridRowValidated ;
194- _grid . RowsRemoved += HandleGridRowsRemoved ;
243+ foreach ( var contribution in _model . Contributions )
244+ _grid . Rows . Add ( contribution . ContributorName , contribution . Role . Name , contribution . Date , contribution . Comments ) ;
245+
246+ if ( _grid . Rows . Count > 0 )
247+ _grid . CurrentCell = _grid [ 0 , 0 ] ;
248+
249+ _grid . IsDirty = false ;
250+
251+ _grid . ResumeLayout ( ) ; // Re-enable layout updates
252+
253+ _grid . RowValidated += HandleGridRowValidated ;
254+ _grid . RowsRemoved += HandleGridRowsRemoved ;
255+ }
256+ catch ( InvalidOperationException ex )
257+ {
258+ Logger . WriteError ( "Error updating contribution list" , ex ) ;
259+ // REVIEW: Then what? Retry every second until this succeeds???
260+ }
195261 }
196262
197263 /// ------------------------------------------------------------------------------------
@@ -277,7 +343,7 @@ private void HandleGridRowValidating(object sender, DataGridViewCellCancelEventA
277343
278344 var kvp = ValidatingContributor ( this , contribution , e ) ;
279345
280- if ( ! string . IsNullOrEmpty ( kvp . Key ) )
346+ if ( ! IsNullOrEmpty ( kvp . Key ) )
281347 {
282348 _msgWindow ??= new FadingMessageWindow ( ) ;
283349
@@ -390,12 +456,12 @@ private void HandleGridCellEndEdit(object sender, DataGridViewCellEventArgs e)
390456 // is the current text an exact match for the autocomplete list?
391457 var list = txtBox . AutoCompleteCustomSource . Cast < object > ( ) . ToList ( ) ;
392458 var found = list . FirstOrDefault ( item =>
393- String . Equals ( item . ToString ( ) , txtBox . Text , StringComparison . CurrentCulture ) ) ;
459+ string . Equals ( item . ToString ( ) , txtBox . Text , StringComparison . CurrentCulture ) ) ;
394460
395461 if ( found == null )
396462 {
397463 // is the current text a match except for case for the autocomplete list?
398- found = list . FirstOrDefault ( item => String . Equals ( item . ToString ( ) ,
464+ found = list . FirstOrDefault ( item => string . Equals ( item . ToString ( ) ,
399465 txtBox . Text , StringComparison . CurrentCultureIgnoreCase ) ) ;
400466 if ( found != null )
401467 {
@@ -442,7 +508,19 @@ private void DeleteRow(int rowIndex)
442508 /// <remarks>SP-874: Localize column headers</remarks>
443509 public void SetColumnHeaderText ( int columnIndex , string headerText )
444510 {
445- _grid . Columns [ columnIndex ] . HeaderText = headerText ;
511+ if ( _model == null )
512+ {
513+ if ( IsDisposed )
514+ throw new ObjectDisposedException ( Name ?? GetType ( ) . Name ) ;
515+
516+ throw new InvalidOperationException (
517+ $ "{ nameof ( Initialize ) } must be called to set the model first.") ;
518+ }
519+
520+ if ( InvokeRequired )
521+ BeginInvoke ( new Action ( ( ) => { _grid . Columns [ columnIndex ] . HeaderText = headerText ; } ) ) ;
522+ else
523+ _grid . Columns [ columnIndex ] . HeaderText = headerText ;
446524 }
447525
448526 /// <remarks>SP-874: Localize column headers</remarks>
@@ -453,7 +531,9 @@ public void SetLocalizationExtender(L10NSharpExtender extender)
453531 extender . SetLocalizingId ( _grid , "ContributorsEditorGrid" ) ;
454532 }
455533
456- /// <remarks>We need to be able to adjust the visual properties to match the hosting program</remarks>
534+ /// <remarks>We need to be able to adjust the visual properties to match the hosting program.
535+ /// If used in code that could run on a thread other than the main UI thread, caller is
536+ /// responsible for invoking on UI thread if needed.</remarks>
457537 public BetterGrid Grid => _grid ;
458538 }
459539}
0 commit comments