Skip to content

Commit ec0def7

Browse files
committed
Perfomance optimisation in computation of trimmed text measurement.
1 parent 6552d78 commit ec0def7

File tree

1 file changed

+109
-48
lines changed

1 file changed

+109
-48
lines changed

source/SuggestBoxLib/PathTrimmingTextBlock.cs

Lines changed: 109 additions & 48 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ namespace SuggestBoxLib
44
using System.Globalization;
55
using System.Windows;
66
using System.Windows.Controls;
7+
using System.Windows.Data;
78

89
/// <summary>
910
/// Enum for specifying where the ellipsis should appear.
@@ -50,16 +51,20 @@ public class PathTrimmingTextBlock : TextBlock
5051
DependencyProperty.Register("Path",
5152
typeof(string),
5253
typeof(PathTrimmingTextBlock),
53-
new UIPropertyMetadata(string.Empty));
54+
new UIPropertyMetadata(string.Empty, OnPathChanged));
5455

5556
/// <summary>
5657
/// Implements the backing store of the <see cref="ShowElipses"/> dependency property.
5758
/// </summary>
5859
public static readonly DependencyProperty ShowElipsesProperty =
5960
DependencyProperty.Register("ShowElipses", typeof(EllipsisPlacement),
60-
typeof(PathTrimmingTextBlock), new PropertyMetadata(EllipsisPlacement.None));
61+
typeof(PathTrimmingTextBlock), new PropertyMetadata(EllipsisPlacement.None, OnShowElipsesChanged));
6162

62-
private FrameworkElement mContainer;
63+
private FrameworkElement _Container;
64+
65+
private readonly TextBlock _MeasureBlock;
66+
private double _lastMeasuredWith;
67+
private string _lastString;
6368
#endregion fields
6469

6570
#region constructor
@@ -68,7 +73,13 @@ public class PathTrimmingTextBlock : TextBlock
6873
/// </summary>
6974
public PathTrimmingTextBlock()
7075
{
71-
this.mContainer = null;
76+
_MeasureBlock = new TextBlock();
77+
BindOneWay(this, "Style", _MeasureBlock, TextBlock.StyleProperty);
78+
BindOneWay(this, "FontWeight", _MeasureBlock, TextBlock.FontWeightProperty);
79+
BindOneWay(this, "FontStyle", _MeasureBlock, TextBlock.FontStyleProperty);
80+
BindOneWay(this, "FontStretch", _MeasureBlock, TextBlock.FontStretchProperty);
81+
BindOneWay(this, "FontSize", _MeasureBlock, TextBlock.FontSizeProperty);
82+
BindOneWay(this, "FontFamily", _MeasureBlock, TextBlock.FontFamilyProperty);
7283

7384
this.Loaded += new RoutedEventHandler(this.PathTrimmingTextBlock_Loaded);
7485
this.Unloaded += new RoutedEventHandler(this.PathTrimmingTextBlock_Unloaded);
@@ -97,6 +108,37 @@ public EllipsisPlacement ShowElipses
97108
#endregion properties
98109

99110
#region methods
111+
/// <summary>
112+
/// Updates the trimmed text based on current measurements, text input, and Show Ellipses parameter.
113+
/// </summary>
114+
protected virtual void UpdateText()
115+
{
116+
if (_Container != null)
117+
this.Text = this.GetTrimmedPath(this.Path, _Container.ActualWidth, this.ShowElipses);
118+
//// else
119+
//// throw new InvalidOperationException("PathTrimmingTextBlock must have a container such as a Grid.");
120+
}
121+
122+
/// <summary>
123+
/// Update measured and trimmed text output if text input has changed.
124+
/// </summary>
125+
/// <param name="d"></param>
126+
/// <param name="e"></param>
127+
private static void OnPathChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
128+
{
129+
(d as PathTrimmingTextBlock).UpdateText();
130+
}
131+
132+
/// <summary>
133+
/// Update measured and trimmed text output if text trimming option has changed.
134+
/// </summary>
135+
/// <param name="d"></param>
136+
/// <param name="e"></param>
137+
private static void OnShowElipsesChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
138+
{
139+
(d as PathTrimmingTextBlock).UpdateText();
140+
}
141+
100142
/// <summary>
101143
/// Textblock is constructed and start its live - lets attach to the
102144
/// size changed event handler of the containing parent.
@@ -109,7 +151,7 @@ private void PathTrimmingTextBlock_Loaded(object sender, RoutedEventArgs e)
109151
if (this.Parent is FrameworkElement)
110152
{
111153
p = (FrameworkElement)this.Parent;
112-
this.mContainer = p;
154+
_Container = p;
113155
}
114156
else
115157
{
@@ -126,15 +168,15 @@ private void PathTrimmingTextBlock_Loaded(object sender, RoutedEventArgs e)
126168
break;
127169
}
128170

129-
this.mContainer = p;
171+
_Container = p;
130172
}
131173
}
132174

133-
if (this.mContainer != null)
175+
if (_Container != null)
134176
{
135-
this.mContainer.SizeChanged += new SizeChangedEventHandler(this.container_SizeChanged);
177+
_Container.SizeChanged += new SizeChangedEventHandler(this.container_SizeChanged);
136178

137-
this.Text = this.GetTrimmedPath(this.mContainer.ActualWidth, this.ShowElipses);
179+
UpdateText();
138180
}
139181
//// else
140182
//// throw new InvalidOperationException("PathTrimmingTextBlock must have a container such as a Grid.");
@@ -147,8 +189,8 @@ private void PathTrimmingTextBlock_Loaded(object sender, RoutedEventArgs e)
147189
/// <param name="e"></param>
148190
private void PathTrimmingTextBlock_Unloaded(object sender, RoutedEventArgs e)
149191
{
150-
if (this.mContainer != null)
151-
this.mContainer.SizeChanged -= this.container_SizeChanged;
192+
if (_Container != null)
193+
_Container.SizeChanged -= this.container_SizeChanged;
152194
}
153195

154196
/// <summary>
@@ -158,8 +200,7 @@ private void PathTrimmingTextBlock_Unloaded(object sender, RoutedEventArgs e)
158200
/// <param name="e"></param>
159201
private void container_SizeChanged(object sender, SizeChangedEventArgs e)
160202
{
161-
if (this.mContainer != null)
162-
this.Text = this.GetTrimmedPath(this.mContainer.ActualWidth, this.ShowElipses);
203+
UpdateText();
163204
}
164205

165206
/// <summary>
@@ -170,71 +211,83 @@ private void container_SizeChanged(object sender, SizeChangedEventArgs e)
170211
private void PathTrimmingTextBlock_IsVisibleChanged(object sender,
171212
DependencyPropertyChangedEventArgs e)
172213
{
173-
if (this.mContainer != null && (bool)e.NewValue == true)
174-
{
175-
this.Text = this.GetTrimmedPath(this.mContainer.ActualWidth, this.ShowElipses);
176-
}
214+
if ((bool)e.NewValue == true)
215+
UpdateText();
177216
}
178217

179218
/// <summary>
180219
/// Compute the text to display (with ellipsis) that fits the ActualWidth of the container
181220
/// </summary>
182-
/// <param name="width"></param>
221+
/// <param name="inputString">Input string to measure whether it fits into container width or not.</param>
222+
/// <param name="containerWidth">ActualWidth restriction by container element (eg.: Grid)</param>
183223
/// <param name="placement"></param>
184224
/// <returns></returns>
185-
private string GetTrimmedPath(double width,
225+
private string GetTrimmedPath(string inputString,
226+
double containerWidth,
186227
EllipsisPlacement placement)
187228
{
229+
// Lets not measure the same thing twice
230+
if (Math.Abs(_lastMeasuredWith - containerWidth) <= 0.5 &&
231+
string.Compare(_lastString, inputString) == 0)
232+
return _MeasureBlock.Text;
233+
234+
_lastMeasuredWith = containerWidth;
235+
_lastString = inputString;
236+
188237
string filename = string.Empty;
189238
string directory = string.Empty;
190239

191240
switch (placement)
192241
{
193242
// We don't want no ellipses to be shown for a string shortener
194243
case EllipsisPlacement.None:
195-
return this.Path;
244+
_MeasureBlock.Text = inputString;
245+
return inputString;
196246

197247
// Try to show a nice ellipses somewhere in the middle of the string
198248
case EllipsisPlacement.Center:
199249
try
200250
{
201-
if (string.IsNullOrEmpty(this.Path) == false)
251+
if (string.IsNullOrEmpty(inputString) == false)
202252
{
203-
if (this.Path.Contains(string.Empty + System.IO.Path.DirectorySeparatorChar))
253+
if (inputString.Contains(string.Empty + System.IO.Path.DirectorySeparatorChar))
204254
{
205255
// Lets try to display the file name with priority
206-
filename = System.IO.Path.GetFileName(this.Path);
207-
directory = System.IO.Path.GetDirectoryName(this.Path);
256+
filename = System.IO.Path.GetFileName(inputString);
257+
directory = System.IO.Path.GetDirectoryName(inputString);
208258
}
209259
else
210260
{
211261
// Cut this right in the middle since it does not seem to hold path info
212-
int len = this.Path.Length;
213-
int firstLen = this.Path.Length / 2;
214-
filename = this.Path.Substring(0, firstLen);
262+
int len = inputString.Length;
263+
int firstLen = inputString.Length / 2;
264+
filename = inputString.Substring(0, firstLen);
215265

216-
if (this.Path.Length >= (firstLen + 1))
217-
directory = this.Path.Substring(firstLen);
266+
if (inputString.Length >= (firstLen + 1))
267+
directory = inputString.Substring(firstLen);
218268
}
219269
}
220270
else
271+
{
272+
_MeasureBlock.Text = string.Empty;
221273
return string.Empty;
274+
}
222275
}
223276
catch (Exception)
224277
{
225-
directory = this.Path;
278+
directory = inputString;
226279
filename = string.Empty;
227280
}
228281
break;
229282

230283
case EllipsisPlacement.Left:
231-
directory = this.Path;
284+
directory = inputString;
232285
filename = string.Empty;
233286
break;
234287

235288
case EllipsisPlacement.Right:
236289
directory = string.Empty;
237-
filename = this.Path;
290+
filename = inputString;
238291
break;
239292

240293
default:
@@ -248,20 +301,12 @@ private string GetTrimmedPath(double width,
248301
if (placement == EllipsisPlacement.Left)
249302
indexString = 1;
250303

251-
TextBlock block = new TextBlock();
252-
block.Style = this.Style;
253-
block.FontWeight = this.FontWeight;
254-
block.FontStyle = this.FontStyle;
255-
block.FontStretch = this.FontStretch;
256-
block.FontSize = this.FontSize;
257-
block.FontFamily = this.FontFamily;
258-
259304
do
260305
{
261-
block.Text = FormatWith(placement, directory, filename);
262-
block.Measure(new Size(double.PositiveInfinity, double.PositiveInfinity));
306+
_MeasureBlock.Text = FormatWith(placement, directory, filename);
307+
_MeasureBlock.Measure(new Size(double.PositiveInfinity, double.PositiveInfinity));
263308

264-
widthOK = block.DesiredSize.Width < width;
309+
widthOK = _MeasureBlock.DesiredSize.Width < containerWidth;
265310

266311
if (widthOK == false)
267312
{
@@ -273,7 +318,10 @@ private string GetTrimmedPath(double width,
273318
filename = filename.Substring(indexString, filename.Length - 1);
274319
}
275320
else
321+
{
322+
_MeasureBlock.Text = string.Empty;
276323
return string.Empty;
324+
}
277325
}
278326
else
279327
{
@@ -285,12 +333,12 @@ private string GetTrimmedPath(double width,
285333
while (widthOK == false);
286334

287335
if (changedWidth == false)
288-
return this.Path;
289-
290-
if (block != null) // Optimize for speed
291-
return block.Text;
336+
{
337+
_MeasureBlock.Text = inputString;
338+
return inputString;
339+
}
292340

293-
return FormatWith(placement, directory, filename);
341+
return _MeasureBlock.Text;
294342
}
295343

296344
/// <summary>
@@ -324,6 +372,19 @@ public static string FormatWith(EllipsisPlacement placing,
324372

325373
return string.Format(CultureInfo.InvariantCulture, formatString, args);
326374
}
375+
376+
private void BindOneWay(FrameworkElement soureOfBinding,
377+
string sourcePathName,
378+
FrameworkElement destinationOfBinding,
379+
DependencyProperty destinationProperty)
380+
{
381+
Binding binding = new Binding();
382+
binding.Path = new PropertyPath(sourcePathName);
383+
binding.Source = soureOfBinding;
384+
binding.UpdateSourceTrigger = UpdateSourceTrigger.PropertyChanged;
385+
binding.Mode = BindingMode.OneWay;
386+
BindingOperations.SetBinding(destinationOfBinding, destinationProperty, binding);
387+
}
327388
#endregion methods
328389
}
329390
}

0 commit comments

Comments
 (0)