[WPF]資料繫結時為何會出現StringFormat失效

czwy發表於2024-08-30

在資料繫結過程中,我們經常會使用StringFormat對要顯示的資料進行格式化,以便獲得更為直觀的展示效果,但在某些情況下格式化操作並未生效,例如 ButtonContent屬性以及ToolTip屬性繫結資料進行StringFormat時是無效的。首先回顧一下StringFormat的基本用法。

StringFormat的用法

StringFormatBindingBase的屬性,指定如果繫結值顯示為字串,應如何設定該繫結的格式。因此,BindingBase 的三個子類:BindingMultiBindingPriorityBinding都可以對繫結資料進行格式化。

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},否則不需要,如第二個示例一樣。
如果設定 ConverterStringFormat屬性,則首先將轉換器應用於資料值,然後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屬性格式化失效的原因

ButtonContent 屬性可以用字串賦值並顯示在按鈕上,但是使用 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 類計算繫結表示式值時獲取到 StringFormatnull,也就不會進行格式化了。
image

ButtonContent 屬性雖然可以用字串賦值,但它其實的 Object 型別。因此,在檢測有效的 StringFormat 表示式時直接過濾了。ToolTip也同樣是 Object 型別。
image

解決方法

對於 Content 這種 Object 型別的屬性繫結字串並且需要格式化時,可以採用以下三種方式解決:

  1. 最通用的方法就是自定義 ValueConverter,在 ValueConverter 中對字串進行格式化;
  2. 繫結到其他可進行 StringFormat 的屬性上,比如 TextBlockText 屬性進行格式化,ToolTip 繫結到 Text 上;
  3. 既然是 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 型別。

相關文章