Salut !
Dans beaucoup de mes programmes j’intègre une fenêtre de log dans laquelle je prompt un peu tout ce que je souhaite (Info d’opération, warning, output standard et/ou erreur de certain process que je lance, etc.)
J'utilise tout le temps un control WPF Textbox, en readonly et multiline.
Dans 99% des cas le log fonctionne parfaitement (auto-scroll actif ou non, l'auto-scroll exploitant le ScrollViewer de la TextBox)
Le 1% des cas restants sont principalement la saturation de log.
En effet quand j’exécute un process externe et que celui-ci me retourne massivement du texte en stdout (boucle infini, prompt continu, ...), mon application C# freeze le temps que le log se calme.
Me disant que mon implémentation du système de log est mauvaise, j'ai créé une solution avec 3 façon différentes de logger du texte :
dont voici les sources :
Le XAML
et le CS:
Code : Sélectionner tout - Visualiser dans une fenêtre à part
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43 <Window x:Class="LogFloodTest.MainWindow" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" Title="MainWindow" Height="436" Width="880" DataContext="{Binding RelativeSource={RelativeSource Self}}"> <Grid> <Grid.ColumnDefinitions> <ColumnDefinition Width="222*" /> <ColumnDefinition Width="222*" /> <ColumnDefinition Width="222*" /> </Grid.ColumnDefinitions> <Grid.RowDefinitions> <RowDefinition Height="340*" /> <RowDefinition Height="76" /> </Grid.RowDefinitions> <TextBox Margin="1" Name="textBox1" AcceptsReturn="True" AcceptsTab="True" VerticalScrollBarVisibility="Auto" /> <TextBox Grid.Column="1" Margin="1" Name="textBox2" AcceptsReturn="True" AcceptsTab="True" Text="{Binding Path=LogText2, Mode=OneWay}" VerticalScrollBarVisibility="Auto" TextChanged="textBox2_TextChanged" /> <TextBox Grid.Column="2" Margin="1" Name="textBox3" AcceptsReturn="True" AcceptsTab="True" Text="{Binding Path=LogText3, Mode=OneWay}" TextChanged="textBox3_TextChanged" VerticalScrollBarVisibility="Auto" /> <GroupBox Grid.Row="1" Header="Full CodeBehind" Margin="0,-2,0,0" Name="groupBox1"> <Grid> <Button Content="Run Flood" Height="23" HorizontalAlignment="Left" Margin="6,0,0,15" Name="button1" VerticalAlignment="Bottom" Width="75" Click="button1_Click" /> <CheckBox Content="Auto-Scroll" Height="16" HorizontalAlignment="Left" Margin="87,17,0,0" Name="AutoScroll_checkBox1" VerticalAlignment="Top" IsChecked="{Binding Path=ScrollCheckBox1, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}" /> <CheckBox Content="Use TextBox.Append Method" Height="16" HorizontalAlignment="Left" Margin="87,3,0,0" Name="Append_checkBox1" VerticalAlignment="Top" IsChecked="{Binding Path=AppendCheckBox1, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}" /> <CheckBox Content="Use Invoke (not BeginInvoke)" Height="16" HorizontalAlignment="Left" IsChecked="{Binding Path=InvokeCheckBox1, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}" Margin="87,38,0,0" Name="checkBox1" VerticalAlignment="Top" /> </Grid> </GroupBox> <GroupBox Header="Text DataBounded (Not the scroll)" Margin="0,-2,0,0" Name="groupBox2" Grid.Column="1" Grid.Row="1"> <Grid> <Button Content="Run Flood" Height="23" HorizontalAlignment="Left" Margin="6,0,0,15" Name="button2" VerticalAlignment="Bottom" Width="75" Click="button2_Click" /> <CheckBox Content="Auto-Scroll" Height="16" HorizontalAlignment="Left" Margin="87,17,0,0" Name="AutoScroll_checkBox2" VerticalAlignment="Top" IsChecked="{Binding Path=ScrollCheckBox2, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}" /> <CheckBox Content="Use StringBuilder to Append" Height="16" HorizontalAlignment="Left" Margin="87,3,0,0" Name="Append_checkBox2" VerticalAlignment="Top" IsChecked="{Binding Path=AppendCheckBox2, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}" /> <CheckBox Content="Use Invoke (not BeginInvoke)" Height="16" HorizontalAlignment="Left" IsChecked="{Binding Path=InvokeCheckBox2, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}" Margin="87,38,0,0" Name="checkBox2" VerticalAlignment="Top" /> </Grid> </GroupBox> <GroupBox Header="DataBounded + Event" Margin="0,-2,0,0" Name="groupBox3" Grid.Column="2" Grid.Row="1"> <Grid> <Button Content="Run Flood" Height="23" HorizontalAlignment="Left" Margin="6,0,0,15" Name="button3" VerticalAlignment="Bottom" Width="75" Click="button3_Click" /> <CheckBox Content="Auto-Scroll" Height="16" HorizontalAlignment="Left" Margin="87,17,0,0" Name="AutoScroll_checkBox3" VerticalAlignment="Top" IsChecked="{Binding Path=ScrollCheckBox3, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}" /> <CheckBox Content="Use StringBuilder to Append" Height="16" HorizontalAlignment="Left" Margin="87,3,0,0" Name="Append_checkBox3" VerticalAlignment="Top" IsChecked="{Binding Path=AppendCheckBox3, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}" /> </Grid> </GroupBox> </Grid> </Window>
Dans ce code il y a un thread qui est créé et qui se charge de flooder une des TextBox (en fonction de celle sélectionnée) durant 2000 ms.
Code : Sélectionner tout - Visualiser dans une fenêtre à part
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325 using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.Windows; using System.Windows.Controls; using System.Windows.Data; using System.Windows.Documents; using System.Windows.Input; using System.Windows.Media; using System.Windows.Media.Imaging; using System.Windows.Navigation; using System.Windows.Shapes; using System.Threading; using System.ComponentModel; namespace LogFloodTest { /// <summary> /// Interaction logic for MainWindow.xaml /// </summary> public partial class MainWindow : Window, INotifyPropertyChanged { #region r_INotifyPropertyChanged public event PropertyChangedEventHandler PropertyChanged; public void NotifyChange(String _PropertyName) { if (PropertyChanged != null) PropertyChanged(this, new PropertyChangedEventArgs(_PropertyName)); } #endregion public bool InvokeRequired { get { return (this.Dispatcher.Thread != System.Threading.Thread.CurrentThread); } } enum e_FloodType { ONE = 0, TWO, THREE, MAX } private bool m_ScrollCheckBox1 = false; public bool ScrollCheckBox1 {get { return m_ScrollCheckBox1; } set { m_ScrollCheckBox1 = value; NotifyChange("ScrollCheckBox1"); }} private bool m_ScrollCheckBox2 = false; public bool ScrollCheckBox2 {get { return m_ScrollCheckBox2; } set { m_ScrollCheckBox2 = value; NotifyChange("ScrollCheckBox2"); }} private bool m_ScrollCheckBox3 = false; public bool ScrollCheckBox3 {get { return m_ScrollCheckBox3; } set { m_ScrollCheckBox3 = value; NotifyChange("ScrollCheckBox3"); }} private bool m_AppendCheckBox1 = false; public bool AppendCheckBox1 {get { return m_AppendCheckBox1; } set { m_AppendCheckBox1 = value; NotifyChange("AppendCheckBox1"); }} private bool m_AppendCheckBox2 = false; public bool AppendCheckBox2 {get { return m_AppendCheckBox2; } set { m_AppendCheckBox2 = value; NotifyChange("AppendCheckBox2"); }} private bool m_AppendCheckBox3 = false; public bool AppendCheckBox3 {get { return m_AppendCheckBox3; } set { m_AppendCheckBox3 = value; NotifyChange("AppendCheckBox3"); }} private bool m_InvokeCheckBox1 = false; public bool InvokeCheckBox1 {get { return m_InvokeCheckBox1; } set { m_InvokeCheckBox1 = value; NotifyChange("InvokeCheckBox1"); }} private bool m_InvokeCheckBox2 = false; public bool InvokeCheckBox2 {get { return m_InvokeCheckBox2; } set { m_InvokeCheckBox2 = value; NotifyChange("InvokeCheckBox2"); }} private bool m_InvokeCheckBox3 = false; public bool InvokeCheckBox3 {get { return m_InvokeCheckBox3; } set { m_InvokeCheckBox3 = value; NotifyChange("InvokeCheckBox3"); }} private String m_LogText2 = ""; public String LogText2 {get { return m_LogText2; } set { m_LogText2 = value; NotifyChange("LogText2"); }} private String m_LogText3 = ""; public String LogText3 {get { return m_LogText3; } set { m_LogText3 = value; NotifyChange("LogText3"); }} private e_FloodType m_LastRunningType = e_FloodType.MAX; private Thread m_FloodThread = null; public MainWindow() { InitializeComponent(); } private void button1_Click(object sender, RoutedEventArgs e) { if (button1.Content == "Cancel") KillMe(e_FloodType.ONE); else RunFlood(e_FloodType.ONE); } private void button2_Click(object sender, RoutedEventArgs e) { if (button2.Content == "Cancel") KillMe(e_FloodType.TWO); else RunFlood(e_FloodType.TWO); } private void button3_Click(object sender, RoutedEventArgs e) { if (button3.Content == "Cancel") KillMe(e_FloodType.THREE); else RunFlood(e_FloodType.THREE); } private void RunFlood(e_FloodType _Type) { if (m_LastRunningType != e_FloodType.MAX) { KillRunning(); } Button btn = null; if (_Type == e_FloodType.ONE) btn = button1; else if (_Type == e_FloodType.TWO) btn = button2; else if (_Type == e_FloodType.THREE) btn = button3; if (btn != null) btn.Content = "Cancel"; m_LastRunningType = _Type; m_FloodThread = new Thread(new ThreadStart(delegate { try { DateTime startTime = DateTime.Now; for (int i = 0; ; ++i) { bool haveToBreak = false; String strToAppend = " - " + i + " New line of flood log that will increase each time: " + i + "\r\n"; // Check timeout TimeSpan ts = DateTime.Now - startTime; int maxToWait = 2000; if (ts.TotalMilliseconds > maxToWait) { haveToBreak = true; strToAppend = "Auto flood auto ended because of time > " + maxToWait + " millisecond\r\n"; } if (_Type == e_FloodType.ONE) FloodOne(strToAppend); else if (_Type == e_FloodType.TWO) FloodTwo(strToAppend); else if (_Type == e_FloodType.THREE) FloodThree(strToAppend); if (haveToBreak == true) break; } } catch (System.Exception ex) { MessageBox.Show("Exception raised: " + ex.Message); } SyncKill(); })) { IsBackground = true }; m_FloodThread.Start(); } private void KillRunning() { if (m_LastRunningType == e_FloodType.MAX) return; KillMe(m_LastRunningType); } private void SyncKill() { if (this.InvokeRequired == true) { this.Dispatcher.Invoke(new Action(SyncKill)); return; } KillRunning(); } private void KillMe(e_FloodType _Type) { Button btn = null; if (_Type == e_FloodType.ONE) btn = button1; else if (_Type == e_FloodType.TWO) btn = button2; else if (_Type == e_FloodType.THREE) btn = button3; if (btn != null) btn.Content = "Run Flood"; try { if (m_FloodThread != null) m_FloodThread.Abort(); } catch { } m_FloodThread = null; m_LastRunningType = e_FloodType.MAX; } private void FloodOne(String _ToAdd) { if (this.InvokeRequired == true) { if (InvokeCheckBox1 == true) this.Dispatcher.Invoke(new Action<String>(FloodOne), new Object[] {_ToAdd}); else this.Dispatcher.BeginInvoke(new Action<String>(FloodOne), new Object[] { _ToAdd }); return; } if (AppendCheckBox1 == true) textBox1.AppendText(_ToAdd); else textBox1.Text += _ToAdd; if (ScrollCheckBox1 == true) { textBox1.CaretIndex = textBox1.Text.Length; textBox1.ScrollToEnd(); } } private StringBuilder m_Log2StringBuilder = new StringBuilder(); private void FloodTwo(String _ToAdd) { m_Log2StringBuilder.Append(_ToAdd); if (AppendCheckBox1 == true) LogText2 = m_Log2StringBuilder.ToString(); else LogText2 += _ToAdd; if (ScrollCheckBox2 == true) { ScrollLog2ToEnd(); } } private void ScrollLog2ToEnd() { if (this.InvokeRequired == true) { if (InvokeCheckBox2 == true) this.Dispatcher.Invoke(new Action(ScrollLog2ToEnd)); else this.Dispatcher.BeginInvoke(new Action(ScrollLog2ToEnd)); return; } textBox2.CaretIndex = textBox2.Text.Length; textBox2.ScrollToEnd(); } private StringBuilder m_Log3StringBuilder = new StringBuilder(); private void FloodThree(String _ToAdd) { m_Log3StringBuilder.Append(_ToAdd); if (AppendCheckBox1 == true) LogText3 = m_Log2StringBuilder.ToString(); else LogText3 += _ToAdd; } private void textBox3_TextChanged(object sender, TextChangedEventArgs e) { if (ScrollCheckBox3 == true) { textBox3.CaretIndex = textBox3.Text.Length; textBox3.ScrollToEnd(); } if (textBox3.Text.Length == 0) m_Log3StringBuilder.Clear(); } private void textBox2_TextChanged(object sender, TextChangedEventArgs e) { if (textBox2.Text.Length == 0) m_Log2StringBuilder.Clear(); } } }
Cas numéro 1 :
Utilisation des Properties public des TextBox pour mettre à jour le contenu.
Cette solution nécessite une re-syncro du thread appelant (Thread de Flood) avec le Thread du control WPF (Main Thread).
3 options sont disponibles :
- Utilisation de la fonction AppendText de la TextBox (à la place de l'operateur += de textBox1.Text)
- Utilisation de this.Dispatcher.Invoke à la place de this.Dispatcher.BeginInvoke (force l'appel synchrone de l'update du control)
- l'Auto-scroll (qui déplace le Caret à la fin, puis scroll la TextBox)
Cas numéro 2 :
Le texte de la TextBox est Databindée : Text="{Binding Path=LogText2, Mode=OneWay}"
La mise à jour du texte se fait donc par un PropertyChangedEventHandler.
Cette solution nécessite une re-syncro du thread appelant (Thread de Flood) avec le Thread du control WPF (Main Thread) dans le cas de la mise à jour de la position du Caret.
3 options sont disponibles :
- Utilisation d'un StringBuilder (et de ses méthodes Append + ToString) (à la place de l'operateur += de textBox2.Text)
- Utilisation de this.Dispatcher.Invoke à la place de this.Dispatcher.BeginInvoke lors de l'auto-scroll du text (force l'appel synchrone de l'update du control)
- l'Auto-scroll (qui déplace le Caret à la fin, puis scroll la TextBox)
Cas numéro 3 :
Le texte de la TextBox est Databindée : Text="{Binding Path=LogText3, Mode=OneWay}"
La mise à jour du texte se fait donc par un PropertyChangedEventHandler.
La mise à jours de la position du Caret lors de l'AutoScroll se fait grâce à l'event TextChanged, celui-ci étant synchrone avec le MainThread, je n'ai donc pas à faire d'Invoke par moi-même
2 options sont disponibles :
- Utilisation d'un StringBuilder (et de ses méthodes Append + ToString) (à la place de l'operateur += de textBox3.Text)
- l'Auto-scroll (qui déplace le Caret à la fin, puis scroll la TextBox)
Résultats :
Dans les 3 cas les résultats ne sont pas bons.
Le flood de log à le dessus sur le rafraichissement de la fenêtre WPF, et donc mon application freeze le temps que le flood s’arrête (durant 2000 ms)
Une différence notable existe entre le Invoke et le BeginInvoke :
Ce dernier étant asynchrone, il rend la main immédiatement après son appel (son exécution se fera lorsque la frame de sync du MainThread reprendra la main)
De ce fait, les appels se font plus rapidement, et les BeginInvoke s'empile (et se dépile quand le MainThread est actif).
C'est là qu'est le problème, l'empilement se fait suffisamment rapidement pour que le MainThread soit saturé d'Invoke dès qu'il reprend la main, et il se retrouve noyer : Une exception sera sans doute levée du type System.OutOfMemoryException
Pour catcher cette exception il faut la gérer dans le App.xaml.cs :
Une bonne différence se ressent lors de l'utilisation du AppendText d'une TextBox, mais quasi aucune lors de l'utilisation d'un StringBuilder
Code : Sélectionner tout - Visualiser dans une fenêtre à part
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21 public partial class App : Application { public App() : base() { this.Dispatcher.UnhandledException += OnDispatcherUnhandledException; } private bool m_MsgBoxShown = false; void OnDispatcherUnhandledException(object sender, System.Windows.Threading.DispatcherUnhandledExceptionEventArgs e) { if (m_MsgBoxShown == false) { m_MsgBoxShown = true; String errorMessage = string.Format("An unhandled exception occurred: {0}", e.Exception.Message); MessageBox.Show(errorMessage, "Error", MessageBoxButton.OK, MessageBoxImage.Error); m_MsgBoxShown = false; } e.Handled = true; } }
Au vu des résultats, je dois mal m'y prendre pour gérer le flood, et malgré mes recherches, je n'ai rien trouvé de concret sur le sujet.
Si quelqu'un sait quoi faire, je suis preneur
Merci !
Partager