在資料繫結過程中,我們經常會使用StringFormat
對要顯示的資料進行格式化,以便獲得更為直觀的展示效果,但在某些情況下格式化操作並未生效,例如 Button
的 Content
屬性以及ToolTip
屬性繫結資料進行StringFormat
時是無效的。首先回顧一下StringFormat
的基本用法。
StringFormat
的用法
StringFormat
是 BindingBase
的屬性,指定如果繫結值顯示為字串,應如何設定該繫結的格式。因此,BindingBase
的三個子類:Binding
、MultiBinding
、PriorityBinding
都可以對繫結資料進行格式化。
Binding
Binding
是最常用的繫結方式,使用StringFormat
遵循.Net格式字串標準即可。例如:
<TextBlock Text="{Binding Price,ElementName=self,StringFormat={}{0:C}}"/>
或者
<TextBlock Text="{Binding TestString,ElementName=self,StringFormat=test:{0}}"/>
其中{0}
表示第一個數值,如果 StringFormat
屬性的值是以花括號開頭,前邊需要有一對花括號 {}
進行轉義,也就是第一個例子中的 {}{0:C}
,否則不需要,如第二個示例一樣。
如果設定 Converter
和 StringFormat
屬性,則首先將轉換器應用於資料值,然後StringFormat
應用該值。
MultiBinding
Binding
繫結時,格式化只能指定一個引數,MultiBinding
繫結時則可指定多個引數。例如:
<TextBlock>
<TextBlock.Text>
<MultiBinding StringFormat="{}{0} {1}">
<Binding Path="FirstName" ElementName="self"/>
<Binding Path="LastName" ElementName="self"/>
</MultiBinding>
</TextBlock.Text>
</TextBlock>
這個例子中 MultiBinding
是由多個子 Binding
組成,StringFormat
僅在設定 MultiBinding
時適用,子 Binding
中雖然也可以設定 StringFormat
,但是會被忽略。
PriorityBinding
相比於前兩種繫結,PriorityBinding
使用的頻率沒那麼高,它的主要作用是按照一定優先順序順序設定繫結列表, 如果最高優先順序繫結在處理時成功返回值,則無需處理列表中的其他繫結。 如果計算優先順序最高的繫結需要很長時間,那麼將會使用成功返回值的次高優先順序,直到優先順序較高的繫結成功返回值。PriorityBinding
和其包含的繫結列表中的子 Binding
也都可以設定 StringFormat
屬性。例如:
<TextBlock
Width="100"
HorizontalAlignment="Center"
Background="Honeydew">
<TextBlock.Text>
<PriorityBinding FallbackValue="defaultvalue" StringFormat="haha:{0}">
<Binding IsAsync="True" Path="SlowestDP" StringFormat="hi:{0}"/>
<Binding IsAsync="True" Path="SlowerDP" />
<Binding Path="FastDP" />
</PriorityBinding>
</TextBlock.Text>
</TextBlock>
與 MultiBinding
不同的是,PriorityBinding
的子 Binding
中的 StringFormat
是會生效的,其規則是優先使用子 Binding
設定的格式,其次才使用PriorityBinding
設定的格式。
Content屬性格式化失效的原因
Button
的 Content
屬性可以用字串賦值並顯示在按鈕上,但是使用 StringFormat
格式化並不會生效。原本我以為是涉及到型別轉換器,在型別轉換過程中處理掉了,但這只是猜測,透過原始碼發現並不是這樣的。在 BindingExpressionBase
中有這樣一段程式碼:
internal virtual bool AttachOverride(DependencyObject target, DependencyProperty dp)
{
_targetElement = new WeakReference(target);
_targetProperty = dp;
DataBindEngine currentDataBindEngine = DataBindEngine.CurrentDataBindEngine;
if (currentDataBindEngine == null || currentDataBindEngine.IsShutDown)
{
return false;
}
_engine = currentDataBindEngine;
DetermineEffectiveStringFormat();
DetermineEffectiveTargetNullValue();
DetermineEffectiveUpdateBehavior();
DetermineEffectiveValidatesOnNotifyDataErrors();
if (dp == TextBox.TextProperty && IsReflective && !IsInBindingExpressionCollection && target is TextBoxBase textBoxBase)
{
textBoxBase.PreviewTextInput += OnPreviewTextInput;
}
if (TraceData.IsExtendedTraceEnabled(this, TraceDataLevel.Attach))
{
TraceData.TraceAndNotifyWithNoParameters(TraceEventType.Warning, TraceData.AttachExpression(TraceData.Identify(this), target.GetType().FullName, dp.Name, AvTrace.GetHashCodeHelper(target)), this);
}
return true;
}
其中第11行呼叫了一個名為 DetermineEffectiveStringFormat
的方法,顧名思義就是檢測有效的 StringFormat
。接下來看看裡邊的邏輯:
internal void DetermineEffectiveStringFormat()
{
Type type = TargetProperty.PropertyType;
if (type != typeof(string))
{
return;
}
string stringFormat = ParentBindingBase.StringFormat;
for (BindingExpressionBase parentBindingExpressionBase = ParentBindingExpressionBase; parentBindingExpressionBase != null; parentBindingExpressionBase = parentBindingExpressionBase.ParentBindingExpressionBase)
{
if (parentBindingExpressionBase is MultiBindingExpression)
{
type = typeof(object);
break;
}
if (stringFormat == null && parentBindingExpressionBase is PriorityBindingExpression)
{
stringFormat = parentBindingExpressionBase.ParentBindingBase.StringFormat;
}
}
if (type == typeof(string) && !string.IsNullOrEmpty(stringFormat))
{
SetValue(Feature.EffectiveStringFormat, Helper.GetEffectiveStringFormat(stringFormat), null);
}
}
這段程式碼的作用就是檢測有效的 StringFormat
,並透過 SetValue
方法儲存起來,從第4~7行程式碼可以看到,一開始就會檢測目標屬性的型別是不是 String
型別,不是的話直接返回,繫結表示式中的 StringFormat
也就不會儲存了。在後續的 BindingExpression
類計算繫結表示式值時獲取到 StringFormat
為 null
,也就不會進行格式化了。
Button
的 Content
屬性雖然可以用字串賦值,但它其實的 Object
型別。因此,在檢測有效的 StringFormat
表示式時直接過濾了。ToolTip
也同樣是 Object
型別。
解決方法
對於 Content
這種 Object
型別的屬性繫結字串並且需要格式化時,可以採用以下三種方式解決:
- 最通用的方法就是自定義
ValueConverter
,在ValueConverter
中對字串進行格式化; - 繫結到其他可進行
StringFormat
的屬性上,比如TextBlock
的Text
屬性進行格式化,ToolTip
繫結到Text
上; - 既然是
Object
型別,那也可把TextBlock
作為Content
的值。
<Button Width="120" Height="30">
<Button.Content>
<TextBlock Text="{Binding TestString,ElementName=self,StringFormat=test:{0}}"/>
</Button.Content>
</Button>
小結
資料繫結時出現StringFormat失效的主要分為兩種情況。一是沒有遵循繫結時StringFormat使用的約束,二是繫結的目標屬性不是 String
型別。