【編輯】WPF 轉換器中如何使用依賴屬性?
獨立觀察員 2025 年 10 月 21 日
0、概述
目前 WPF 的轉換器中無法放置依賴屬性,ConverterParameter 又不支持綁定,所以如果在轉換器場景中需要綁定的話,就有點頭疼了。
遇事不決,問 AI,所以我把這個問題在 GitHub Copilot 中問了下 GPT-5。
它的初始回答簡單明瞭,提供了兩種方案:Freezable 方案 和 MultiBinding 方案。
由於目前轉換器一般都繼承了 MarkupExtension 以方便使用,所以又加入了這個因素,AI 也是絲毫不虛,不過情況就變得複雜了起來……
本文基本就是摘抄原始問答,並未驗證,不過看着像那麼回事,也很有啓發,所以分享給大家。
後續可能會選擇其中合適的方案,改造一兩個目前的轉換器,到時候應該會再發文分享。
1、GPT-5 的初始回答
下面用中文説明並給出可直接使用的示例代碼 —— 我實現了兩種常見做法並展示如何在 XAML 中使用:
1) 讓轉換器繼承自 Freezable,從而可以聲明依賴屬性並對它們使用 Binding;
2) 用 MultiBinding 傳入額外綁定值(當你不想把轉換器當作 Freezable 時的替代方案)。
我也會説明為什麼普通的 IValueConverter 不能直接有可綁定的依賴屬性,以及常見陷阱。
1.1、Freezable 方案
我先給出 Freezable 方案的完整代碼和 XAML 用法。
MyFreezableConverter.cs
using System;
using System.Globalization;
using System.Windows;
using System.Windows.Data;
namespace YourNamespace
{
// 繼承 Freezable 以支持在 XAML 中對轉換器的依賴屬性進行 Binding
public class MyFreezableConverter : Freezable, IValueConverter
{
// 示例依賴屬性:Prefix
public string Prefix
{
get => ( string )GetValue(PrefixProperty);
set => SetValue(PrefixProperty, value);
}
public static readonly DependencyProperty PrefixProperty =
DependencyProperty.Register(
nameof(Prefix),
typeof ( string ),
typeof (MyFreezableConverter),
new PropertyMetadata( string .Empty)
);
// IValueConverter 實現
public object Convert( object value, Type targetType, object parameter, CultureInfo culture)
{
var s = value?.ToString() ?? string .Empty;
var prefix = Prefix ?? string .Empty;
return prefix + s;
}
public object ConvertBack( object value, Type targetType, object parameter, CultureInfo culture)
{
throw new NotSupportedException();
}
// Freezable 必須實現
protected override Freezable CreateInstanceCore()
{
return new MyFreezableConverter();
}
}
}
在 XAML 中這樣使用(把轉換器放在資源並對其依賴屬性做綁定):
ExampleWindow.xaml
< Window x:Class="YourNamespace.ExampleWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="clr-namespace:YourNamespace"
x:Name="Root">
< Window.Resources >
< local:MyFreezableConverter x:Key="MyConverter"
Prefix="{Binding SomePrefixProperty, ElementName=Root}" />
< StackPanel >
< TextBlock Text="{Binding Path=Name, Converter={StaticResource MyConverter}}" />
要點説明(Freezable 方案)
- 普通的 IValueConverter 是普通類,不在可視樹上,不能直接使用 Binding 來更新其字段 / 屬性;將轉換器繼承自 Freezable 後,你可以在轉換器上註冊 DependencyProperty,就能在 XAML 中對這些屬性做 Binding。
- 必須實現 CreateInstanceCore ()。
- 把轉換器聲明在資源中(StaticResource)並給依賴屬性綁定即可。
- 注意:如果轉換器被 freeze(一般情況不會自動 freeze),綁定將不工作;通常通過 XAML 給資源的綁定會保持可變。
1.2、MultiBinding 方案
下面給出另一個常用替代方案:當你只是需要一個可綁定參數,可以使用 MultiBinding,把主值和額外的綁定一起傳入 IMultiValueConverter。
NameAndPrefixMultiConverter.cs
using System;
using System.Globalization;
using System.Windows.Data;
namespace YourNamespace
{
public class NameAndPrefixMultiConverter : IMultiValueConverter
{
public object Convert( object [] values, Type targetType, object parameter, CultureInfo culture)
{
// values[0] = 主綁定(例如 Name), values[1] = Prefix(來自另一個綁定)
var name = values.Length > 0 ? values[0]?.ToString() ?? string .Empty : string .Empty;
var prefix = values.Length > 1 ? values[1]?.ToString() ?? string .Empty : string .Empty;
return prefix + name;
}
public object [] ConvertBack( object value, Type[] targetTypes, object parameter, CultureInfo culture)
{
throw new NotSupportedException();
}
}
}
XAML 使用示例:
ExampleWindow_MultiBinding.xaml
< Window x:Class="YourNamespace.ExampleWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="clr-namespace:YourNamespace"
x:Name="Root">
< Window.Resources >
< local:NameAndPrefixMultiConverter x:Key="NameAndPrefixMultiConverter"/>
< StackPanel >
< TextBlock >
< TextBlock.Text >
< MultiBinding Converter="{StaticResource NameAndPrefixMultiConverter}">
< Binding Path="Name" />
< Binding Path="SomePrefixProperty" ElementName="Root"/>
為什麼不直接給 ConverterParameter 綁定?
- ConverterParameter 不是依賴屬性,不能綁定(在 XAML 中寫 {Binding …} 給 ConverterParameter 一般只會把 Binding 對象本身傳進去,不會被解析為綁定值)。
- 解決辦法就是上面兩種:Freezable(讓轉換器本身具有可綁定的依賴屬性)或 MultiBinding(把參數作為第二個綁定傳入)。
常見陷阱與建議
- 如果轉換器寫成 Freezable 且放在 Application 或 Window 資源中並共享(x:Shared 默認 true),要確認綁定路徑和 ElementName/RelativeSource 指向正確的元素;有時候把資源放在更接近目標的資源字典可以更容易地使用 ElementName。
- 如果轉換器是純 stateless(無需綁定運行時參數),繼續使用普通靜態實例(StaticResource)更節省開銷。
- MultiBinding 在傳入多個值時非常直接且常用;但如果轉換器需要很多可綁定屬性,Freezable 比較方便,代碼更清晰。
我已經實現並展示了兩種可行方案的完整代碼和 XAML 示例。接下來我可以:
- 根據你當前項目的具體場景(你希望綁定哪個屬性 / 來自哪個 DataContext / 使用 ElementName 還是綁定到 ViewModel),把示例改寫成適配你代碼的版本;
- 或者把轉換器封裝為 MarkupExtension 的方式(另一種進階方法),如果你要我可以給出該方案的實現與説明。你希望我為你的具體用例提供示例嗎?
2、把轉換器封裝為 MarkupExtension 的方式
下面補上把轉換器封裝為 MarkupExtension 的方式 —— 這樣可以在 XAML 內聯使用轉換器並且支持把 Binding(作為屬性)傳給轉換器。
實現思路是:讓轉換器繼承 Freezable(從而可以聲明 DependencyProperty 並接受 Binding),再寫一個 MarkupExtension 在 ProvideValue 時創建轉換器實例並把傳入的 Binding(BindingBase 類型的屬性)通過 BindingOperations 設置到轉換器的依賴屬性上。這樣就可以在 XAML 中像下面這樣寫: Converter={local:PrefixConverter PrefixBinding={Binding SomePrefix}} 。(dlgcy:應為 local:PrefixConverterExtension ?)
我給出可直接拷貝的完整示例文件(C# + XAML)。
PrefixConverter.cs(同上節的 MyFreezableConverter.cs)
using System;
using System.Globalization;
using System.Windows;
using System.Windows.Data;
namespace YourNamespace
{
// 繼承 Freezable 以支持在轉換器上使用 Binding(通過 DependencyProperty)
public class PrefixConverter : Freezable, IValueConverter
{
public string Prefix
{
get => ( string )GetValue(PrefixProperty);
set => SetValue(PrefixProperty, value);
}
public static readonly DependencyProperty PrefixProperty =
DependencyProperty.Register(
nameof(Prefix),
typeof ( string ),
typeof (PrefixConverter),
new PropertyMetadata( string .Empty)
);
public object Convert( object value, Type targetType, object parameter, CultureInfo culture)
{
var s = value?.ToString() ?? string .Empty;
var prefix = Prefix ?? string .Empty;
return prefix + s;
}
public object ConvertBack( object value, Type targetType, object parameter, CultureInfo culture)
{
throw new NotSupportedException();
}
// Freezable 必須實現
protected override Freezable CreateInstanceCore()
{
return new PrefixConverter();
}
}
}
PrefixConverterExtension.cs
using System;
using System.Windows.Data;
using System.Windows.Markup;
using System.Windows;
namespace YourNamespace
{
// MarkupExtension:在 XAML 中可以傳入靜態值或 Binding(作為 PrefixBinding)
public class PrefixConverterExtension : MarkupExtension
{
// 允許靜態字符串
public string Prefix { get ; set ; }
// 允許傳入 Binding 或其他 BindingBase(例如 Binding/RelativeSource 等)
public BindingBase PrefixBinding { get ; set ; }
public PrefixConverterExtension() { }
// 支持用構造函數傳入靜態前綴: {local:PrefixConverterExtension 'Hello: '}
public PrefixConverterExtension( string prefix)
{
Prefix = prefix;
}
public override object ProvideValue(IServiceProvider serviceProvider)
{
// 每次 ProvideValue 創建一個新的轉換器實例(因此內聯使用時不會被共享)
var conv = new PrefixConverter();
if (PrefixBinding != null )
{
// 把傳入的 Binding 應用到轉換器的依賴屬性上
BindingOperations.SetBinding(conv, PrefixConverter.PrefixProperty, PrefixBinding);
}
else if (Prefix != null )
{
conv.Prefix = Prefix;
}
return conv;
}
}
}
示例 XAML(展示內聯綁定與靜態參數兩種用法):
ExampleWindow.xaml(v2)
< Window x:Class="YourNamespace.ExampleWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="clr-namespace:YourNamespace"
x:Name="Root" Title="示例" Height="200" Width="400">
< StackPanel Margin="12">
< TextBlock Margin="0,0,0,8"
Text="{Binding Name, Converter={local:PrefixConverterExtension 'Static: '}}"/>
< TextBlock >
< TextBlock.Text >
< Binding Path="Name">
< Binding.Converter >
< local:PrefixConverterExtension PrefixBinding="{Binding SomePrefixProperty, ElementName=Root}" />
要點總結與注意事項
- 這種做法的優點:可以在單一表達式裏把 Binding 傳給轉換器的屬性(例如前綴、格式字符串、其它運行時參數),不需要把轉換器放在資源並單獨為其設置綁定。
- 轉換器繼承 Freezable 是關鍵(Freezable 可以作為 Binding 的目標並使用依賴屬性)。
- ProvideValue 中返回的轉換器實例通常是每次創建新實例(內聯用法),因此不會被共享;如果希望在資源中重用,需要注意 x:Shared 和綁定目標(可能需要 x:Shared=”False” 或其他技巧)。
- ConverterParameter 仍然不能綁定;如果你需要把多個可綁定值傳進轉換器,也可以使用 MultiBinding + IMultiValueConverter(我之前已有示例)。
- 當在資源字典中共享實例時要注意 ElementName/RelativeSource 的可見性 / 作用域;內聯 MarkupExtension 更易於在當前上下文裏解析綁定。
我已把上述 MarkupExtension + Freezable converter 的實現和 XAML 示例都列出,你可以直接把兩個 C# 文件加入到項目並在 XAML 中按示例使用。接下來如果你希望,我可以:
- 把該轉換器擴展為通用的帶多個可綁定參數的模板(例如多個 Binding 屬性),或者
- 根據你項目的具體 DataContext / 屬性名,把示例改寫為可直接運行的代碼。
3、擴展為通用的帶多個可綁定參數的模板
下面給出一個可直接複用的「通用多參數可綁定轉換器模板」。思路與要點:
- 使用 Freezable 的子類作為轉換器(可聲明依賴屬性並作為綁定目標)。
- 定義一個可在 XAML 中作為子元素添加的 ConverterParameter(也是 Freezable),包含 Name 與 Value(Value 可綁定)。
- 提供一個抽象基類 MultiParameterConverterBase,子類只需實現帶參數集合的 Convert 邏輯即可。
- 演示一個具體實現 FormatConverter(讀取名為 Format、Prefix、Suffix 的參數),並給出 XAML 使用示例(在 Binding 內聯轉換器並把多個綁定的參數作為子元素傳入)。
下面是可拷貝到項目裏的文件。
ConverterParameter.cs
using System;
using System.Windows;
namespace YourNamespace.Converters
{
// 單個可綁定參數,Freezable 允許 Value 使用 Binding
public class ConverterParameter : Freezable
{
public string Name
{
get => ( string )GetValue(NameProperty);
set => SetValue(NameProperty, value);
}
public static readonly DependencyProperty NameProperty =
DependencyProperty.Register(
nameof(Name),
typeof ( string ),
typeof (ConverterParameter),
new PropertyMetadata( string .Empty)
);
// Value 可以是任意對象,也可以通過 Binding 設置
public object Value
{
get => GetValue(ValueProperty);
set => SetValue(ValueProperty, value);
}
public static readonly DependencyProperty ValueProperty =
DependencyProperty.Register(
nameof(Value),
typeof ( object ),
typeof (ConverterParameter),
new PropertyMetadata( null )
);
protected override Freezable CreateInstanceCore()
{
return new ConverterParameter();
}
}
}
MultiParameterConverterBase.cs
using System;
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
using System.Windows;
using System.Windows.Data;
using System.Windows.Markup;
namespace YourNamespace.Converters
{
// 允許在 XAML 中把 作為子元素添加到 Parameters
[ContentProperty(nameof(Parameters))]
public abstract class MultiParameterConverterBase : Freezable, IValueConverter
{
// 一個 FreezableCollection 用來存放 ConverterParameter(可綁定)
public FreezableCollection Parameters
{
get => (FreezableCollection)GetValue(ParametersProperty);
set => SetValue(ParametersProperty, value);
}
public static readonly DependencyProperty ParametersProperty =
DependencyProperty.Register(
nameof(Parameters),
typeof (FreezableCollection),
typeof (MultiParameterConverterBase),
new PropertyMetadata( null )
);
protected MultiParameterConverterBase()
{
// 確保默認集合存在(方便 XAML child element 添加)
if (GetValue(ParametersProperty) == null )
{
SetValue(ParametersProperty, new FreezableCollection());
}
}
// 子類必須實現創建實例
protected abstract override Freezable CreateInstanceCore();
// 子類實現具體的轉換邏輯(帶參數集合)
protected abstract object ConvertWithParameters( object value, Type targetType, object parameter, CultureInfo culture, IList parameters);
// IValueConverter 接口實現,內部把 Parameters 傳給子類
public object Convert( object value, Type targetType, object parameter, CultureInfo culture)
{
var list = Parameters ?? new FreezableCollection();
return ConvertWithParameters(value, targetType, parameter, culture, list);
}
public object ConvertBack( object value, Type targetType, object parameter, CultureInfo culture)
{
// 默認不支持,子類可以重寫(也可以保持默認)
throw new NotSupportedException();
}
// 輔助:按 Name 找到參數值並轉換為指定類型
protected T GetParameterValue(IList parameters, string name, T defaultValue = default )
{
if (parameters == null || string .IsNullOrEmpty(name)) return defaultValue;
var p = parameters.FirstOrDefault(x => string .Equals(x?.Name, name, StringComparison.Ordinal));
if (p == null ) return defaultValue;
if (p.Value is T t) return t;
try
{
if (p.Value == null ) return defaultValue;
return (T)System.Convert.ChangeType(p.Value, typeof (T), CultureInfo.InvariantCulture);
}
catch
{
return defaultValue;
}
}
}
// 一個示例具體實現:格式化並可使用 Prefix/Suffix
public class FormatConverter : MultiParameterConverterBase
{
protected override Freezable CreateInstanceCore()
{
return new FormatConverter();
}
protected override object ConvertWithParameters( object value, Type targetType, object parameter, CultureInfo culture, IList parameters)
{
// 示例約定:支持 Format、Prefix、Suffix 三個參數(都可綁定)
var format = GetParameterValue< string >(parameters, "Format" , "{0}" );
var prefix = GetParameterValue< string >(parameters, "Prefix" , string .Empty);
var suffix = GetParameterValue< string >(parameters, "Suffix" , string .Empty);
string result;
try
{
result = string .Format(culture ?? CultureInfo.CurrentCulture, format, value);
}
catch
{
// 如果 format 不合法,退回到簡單的 ToString()
result = value?.ToString() ?? string .Empty;
}
return prefix + result + suffix;
}
}
}</converterparameter ...>
ExampleViewModel.cs
using System.ComponentModel;
using System.Runtime.CompilerServices;
namespace YourNamespace
{
public class ExampleViewModel : INotifyPropertyChanged
{
private string _name = "Alice" ;
private string _format = "Name: {0}" ;
private string _prefix = "[" ;
private string _suffix = "]" ;
public string Name
{
get => _name;
set { _name = value; OnPropertyChanged(); }
}
public string FormatString
{
get => _format;
set { _format = value; OnPropertyChanged(); }
}
public string Prefix
{
get => _prefix;
set { _prefix = value; OnPropertyChanged(); }
}
public string Suffix
{
get => _suffix;
set { _suffix = value; OnPropertyChanged(); }
}
public event PropertyChangedEventHandler PropertyChanged;
void OnPropertyChanged([CallerMemberName] string name = null ) => PropertyChanged?.Invoke( this , new PropertyChangedEventArgs(name));
}
}
ExampleWindow.xaml
< Window x:Class="YourNamespace.ExampleWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:conv="clr-namespace:YourNamespace.Converters"
xmlns:local="clr-namespace:YourNamespace"
x:Name="Root"
Title="Multi-Parameter Converter 示例" Height="200" Width="400">
< Window.DataContext >
< local:ExampleViewModel />
< StackPanel Margin="12" VerticalAlignment="Top">
< TextBlock Margin="0,0,0,8">
< TextBlock.Text >
< Binding Path="Name">
< Binding.Converter >
< conv:FormatConverter >
< conv:FormatConverter.Parameters >
< conv:ConverterParameter Name="Format" Value="{Binding FormatString, ElementName=Root}"/>
< conv:ConverterParameter Name="Prefix" Value="{Binding Prefix, ElementName=Root}"/>
< conv:ConverterParameter Name="Suffix" Value="{Binding Suffix, ElementName=Root}"/>
< StackPanel Orientation="Horizontal" Margin="0,8,0,0">
< TextBlock Text="Format:" VerticalAlignment="Center" Margin="0,0,6,0"/>
< TextBox Width="200" Text="{Binding FormatString, UpdateSourceTrigger=PropertyChanged}"/>
< StackPanel Orientation="Horizontal" Margin="0,8,0,0">
< TextBlock Text="Prefix:" VerticalAlignment="Center" Margin="0,0,6,0"/>
< TextBox Width="60" Text="{Binding Prefix, UpdateSourceTrigger=PropertyChanged}"/>
< TextBlock Text="Suffix:" VerticalAlignment="Center" Margin="12,0,6,0"/>
< TextBox Width="60" Text="{Binding Suffix, UpdateSourceTrigger=PropertyChanged}"/>
使用説明與注意事項
- 把這些文件加入你的項目(命名空間按需要調整)。ConverterParameter 和 MultiParameterConverterBase 都是 Freezable,因此 ConverterParameter.Value 可以通過 Binding 設置並在運行時更新。
- 在 XAML 中,MultiParameterConverterBase 聲明瞭 ContentProperty 為 Parameters,可以把多個 <conv:ConverterParameter …/> 放在轉換器內作為子元素,且每個 ConverterParameter 的 Value 可以是 Binding/ElementName/RelativeSource 等任意 Binding。
- 你可以基於 MultiParameterConverterBase 實現任意業務邏輯的轉換器:在 ConvertWithParameters 中讀取需要的參數(通過 Name)並執行邏輯。
- 如果想內聯更簡便的 MarkupExtension(例如像 earlier PrefixConverterExtension 那樣在屬性內直接寫 Converter={local:… PrefixBinding=…}),也可以按同樣思路實現:在 ProvideValue 中創建轉換器實例並通過 BindingOperations.SetBinding 把外部傳入的 Binding 應用到 ConverterParameter.Value 上。上面示例為了簡單與清晰,採用了 XAML 子元素方式(更直觀且不需要額外的 MarkupExtension)。
如果你願意,我可以接着提供:
- 一個通用的 MarkupExtension 版本(允許在單個表達式內傳入多個 Binding 參數);
- 或把示例改成 IMultiValueConverter + MultiBinding 風格的通用模板(若你更偏向把多個值並列傳入轉換器)。
你想接着要哪一個(MarkupExtension 版 或 IMultiValueConverter 版)?
4、可直接複用的 MarkupExtension 版本
下面給出一個可直接複用的 MarkupExtension 版本 —— 它允許在單一表達式或對象元素中內聯創建一個帶多個可綁定參數的轉換器。實現思路:
- 使用 Freezable 的 ConverterParameter(Value 可綁定);
- 使用一個抽象基類 MultiParameterConverterBase(繼承 Freezable 幷包含 Parameters 集合),子類只需實現 ConvertWithParameters;
- 提供 MultiParamConverterExtension(MarkupExtension),它接受 ConverterType(要創建的轉換器類型)並通過子元素接收一組 ConverterParameter,然後在 ProvideValue 時為創建的轉換器克隆並注入這些參數(保持綁定的生效)。
我把主要文件都給出,你可以直接複製到項目並按示例 XAML 使用。
ConverterParameter.cs(v2,好像和 v1 一樣)
using System;
using System.Windows;
namespace YourNamespace.Converters
{
// 單個可綁定參數,Freezable 允許 Value 使用 Binding
public class ConverterParameter : Freezable
{
public string Name
{
get => ( string )GetValue(NameProperty);
set => SetValue(NameProperty, value);
}
public static readonly DependencyProperty NameProperty =
DependencyProperty.Register(
nameof(Name),
typeof ( string ),
typeof (ConverterParameter),
new PropertyMetadata( string .Empty)
);
// Value 可以是任意對象,也可以通過 Binding 設置
public object Value
{
get => GetValue(ValueProperty);
set => SetValue(ValueProperty, value);
}
public static readonly DependencyProperty ValueProperty =
DependencyProperty.Register(
nameof(Value),
typeof ( object ),
typeof (ConverterParameter),
new PropertyMetadata( null )
);
protected override Freezable CreateInstanceCore()
{
return new ConverterParameter();
}
}
}
MultiParameterConverterBase.cs(v2)
using System;
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
using System.Windows;
using System.Windows.Data;
using System.Windows.Markup;
namespace YourNamespace.Converters
{
// 允許直接把 ConverterParameter 作為子元素添加到轉換器(如果你直接把轉換器寫成對象元素)
[ContentProperty(nameof(Parameters))]
public abstract class MultiParameterConverterBase : Freezable, IValueConverter
{
public FreezableCollection Parameters
{
get => (FreezableCollection)GetValue(ParametersProperty);
set => SetValue(ParametersProperty, value);
}
public static readonly DependencyProperty ParametersProperty =
DependencyProperty.Register(
nameof(Parameters),
typeof (FreezableCollection),
typeof (MultiParameterConverterBase),
new PropertyMetadata( null )
);
protected MultiParameterConverterBase()
{
if (GetValue(ParametersProperty) == null )
{
SetValue(ParametersProperty, new FreezableCollection());
}
}
// 子類必須實現創建實例
protected abstract override Freezable CreateInstanceCore();
// 子類實現具體的轉換邏輯(帶參數集合)
protected abstract object ConvertWithParameters( object value, Type targetType, object parameter, CultureInfo culture, IList parameters);
public object Convert( object value, Type targetType, object parameter, CultureInfo culture)
{
var list = Parameters ?? new FreezableCollection();
return ConvertWithParameters(value, targetType, parameter, culture, list);
}
public object ConvertBack( object value, Type targetType, object parameter, CultureInfo culture)
{
throw new NotSupportedException();
}
protected T GetParameterValue(IList parameters, string name, T defaultValue = default )
{
if (parameters == null || string .IsNullOrEmpty(name)) return defaultValue;
var p = parameters.FirstOrDefault(x => string .Equals(x?.Name, name, StringComparison.Ordinal));
if (p == null ) return defaultValue;
if (p.Value is T t) return t;
try
{
if (p.Value == null ) return defaultValue;
return (T)System.Convert.ChangeType(p.Value, typeof (T), CultureInfo.InvariantCulture);
}
catch
{
return defaultValue;
}
}
}
// 示例具體實現:格式化並可使用 Format/Prefix/Suffix 三個參數
public class FormatConverter : MultiParameterConverterBase
{
protected override Freezable CreateInstanceCore()
{
return new FormatConverter();
}
protected override object ConvertWithParameters( object value, Type targetType, object parameter, CultureInfo culture, IList parameters)
{
var format = GetParameterValue< string >(parameters, "Format" , "{0}" );
var prefix = GetParameterValue< string >(parameters, "Prefix" , string .Empty);
var suffix = GetParameterValue< string >(parameters, "Suffix" , string .Empty);
string result;
try
{
result = string .Format(culture ?? CultureInfo.CurrentCulture, format, value);
}
catch
{
result = value?.ToString() ?? string .Empty;
}
return prefix + result + suffix;
}
}
}
MultiParamConverterExtension.cs
using System;
using System.Windows.Data;
using System.Windows.Markup;
namespace YourNamespace.Converters
{
// 作為 MarkupExtension 使用:可以把多個 作為子元素傳入
// 用法(示例):
//
//
//
//
//
// </conv:converterparameter name="prefix" value="{binding prefix, elementname=root}"></conv:converterparameter name="format" value="{binding formatstring, elementname=root}"></conv:multiparamconverterextension convertertype="{x:type conv:formatconverter}">
[ContentProperty(nameof(Parameters))]
public class MultiParamConverterExtension : MarkupExtension
{
// 必填:要創建的轉換器類型,必須繼承自 MultiParameterConverterBase
public Type ConverterType { get ; set ; }
// 子元素接收的參數集合(ConverterParameter 是 Freezable,Value 可綁定)
public FreezableCollection Parameters { get ; } = new FreezableCollection();
public MultiParamConverterExtension() { }
public MultiParamConverterExtension(Type converterType)
{
ConverterType = converterType;
}
public override object ProvideValue(IServiceProvider serviceProvider)
{
if (ConverterType == null )
throw new InvalidOperationException( "ConverterType must be set." );
if (! typeof (MultiParameterConverterBase).IsAssignableFrom(ConverterType))
throw new InvalidOperationException( "ConverterType must derive from MultiParameterConverterBase." );
// 創建轉換器實例(每次 ProvideValue 都創建新的轉換器,避免共享導致綁定混亂)
var converter = (MultiParameterConverterBase)Activator.CreateInstance(ConverterType);
// 克隆每個傳入的 ConverterParameter 並添加到 converter 的 Parameters 集合
foreach ( var p in Parameters)
{
if (p == null ) continue ;
// 使用 Freezable.Clone() 克隆以避免對象共享
var cloned = (ConverterParameter)p.Clone();
converter.Parameters.Add(cloned);
}
return converter;
}
}
}
ExampleWindow.xaml
< Window x:Class="YourNamespace.ExampleWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:conv="clr-namespace:YourNamespace.Converters"
xmlns:local="clr-namespace:YourNamespace"
x:Name="Root"
Title="MultiParam MarkupExtension 示例" Height="240" Width="480">
< Window.DataContext >
< local:ExampleViewModel />
< StackPanel Margin="12">
< TextBlock Margin="0,0,0,12">
< TextBlock.Text >
< Binding Path="Name">
< Binding.Converter >
< conv:MultiParamConverterExtension ConverterType="{x:Type conv:FormatConverter}">
< conv:ConverterParameter Name="Format" Value="{Binding FormatString, ElementName=Root}"/>
< conv:ConverterParameter Name="Prefix" Value="{Binding Prefix, ElementName=Root}"/>
< conv:ConverterParameter Name="Suffix" Value="{Binding Suffix, ElementName=Root}"/>
< StackPanel Orientation="Horizontal" Margin="0,6">
< TextBlock Text="Format:" VerticalAlignment="Center" Margin="0,0,6,0"/>
< TextBox Width="260" Text="{Binding FormatString, UpdateSourceTrigger=PropertyChanged}"/>
< StackPanel Orientation="Horizontal" Margin="0,6">
< TextBlock Text="Prefix:" VerticalAlignment="Center" Margin="0,0,6,0"/>
< TextBox Width="80" Text="{Binding Prefix, UpdateSourceTrigger=PropertyChanged}"/>
< TextBlock Text="Suffix:" VerticalAlignment="Center" Margin="12,0,6,0"/>
< TextBox Width="80" Text="{Binding Suffix, UpdateSourceTrigger=PropertyChanged}"/>
説明、注意事項與擴展建議
- 用法很直觀:在 Binding.Converter 中內聯一個 MultiParamConverterExtension,並以子元素形式寫幾個 <conv:ConverterParameter Name=”…” Value=”{Binding …}”/>。這些參數的 Value 支持任意 Binding(ElementName/RelativeSource/DataContext 都有效)。
- 在 ProvideValue 中為轉換器創建新實例並克隆參數,避免多個目標共享同一參數對象導致綁定衝突。
- 要點:ConverterType 必須派生自 MultiParameterConverterBase;你可以隨意實現更多具體轉換器(例如基於參數做格式化、條件選擇、單位轉換等)。
- 若你希望直接在 Converter={…} 的簡寫內使用(非對象元素語法),XAML 解析器仍允許小量內聯屬性寫法,但傳遞多個參數時對象元素語法(如示例)更清晰可靠。(dlgcy:這也導致和上一小節的效果差別不大)
接下來我可以:
- 把這套代碼改為支持在 MarkupExtension 中直接以屬性傳入簡單靜態參數(例如 Params=”Prefix='[‘;Suffix=’]’ Format=’…'”)並同時支持 Binding;
- 或者把擴展做得更通用(例如允許傳入一個已存在的 converter 實例、或者支持從資源重用並注入參數)。
你希望我先做哪一種擴展?
5、在 MarkupExtension 基礎上增加一個 Params 字符串屬性
我按你的 “第一種” 要求實現了:在之前的 MarkupExtension 基礎上增加一個 Params 字符串屬性,用於在單個屬性中傳入多個靜態參數(例如 Params=”Format=’Name: {0}’; Prefix='[‘; Suffix=’]'”),同時保留通過子元素傳入的 ConverterParameter(支持 Binding)。子元素的參數會覆蓋 Params 中同名的靜態參數,從而同時支持靜態與可綁定參數的混合使用。
下面是我修改 / 新增的文件(可直接拷貝到項目)。説明在每個文件之後給出簡短示例與注意事項。
ConverterParameter.cs(v3)
using System;
using System.Windows;
namespace YourNamespace.Converters
{
// 單個可綁定參數,Freezable 允許 Value 使用 Binding
public class ConverterParameter : Freezable
{
public string Name
{
get => ( string )GetValue(NameProperty);
set => SetValue(NameProperty, value);
}
public static readonly DependencyProperty NameProperty =
DependencyProperty.Register(
nameof(Name),
typeof ( string ),
typeof (ConverterParameter),
new PropertyMetadata( string .Empty)
);
// Value 可以是任意對象,也可以通過 Binding 設置
public object Value
{
get => GetValue(ValueProperty);
set => SetValue(ValueProperty, value);
}
public static readonly DependencyProperty ValueProperty =
DependencyProperty.Register(
nameof(Value),
typeof ( object ),
typeof (ConverterParameter),
new PropertyMetadata( null )
);
protected override Freezable CreateInstanceCore()
{
return new ConverterParameter();
}
}
}
MultiParameterConverterBase.cs(v3)
using System;
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
using System.Windows;
using System.Windows.Data;
using System.Windows.Markup;
namespace YourNamespace.Converters
{
// 允許直接把 ConverterParameter 作為子元素添加到轉換器(如果你直接把轉換器寫成對象元素)
[ContentProperty(nameof(Parameters))]
public abstract class MultiParameterConverterBase : Freezable, IValueConverter
{
public FreezableCollection Parameters
{
get => (FreezableCollection)GetValue(ParametersProperty);
set => SetValue(ParametersProperty, value);
}
public static readonly DependencyProperty ParametersProperty =
DependencyProperty.Register(
nameof(Parameters),
typeof (FreezableCollection),
typeof (MultiParameterConverterBase),
new PropertyMetadata( null )
);
protected MultiParameterConverterBase()
{
if (GetValue(ParametersProperty) == null )
{
SetValue(ParametersProperty, new FreezableCollection());
}
}
// 子類必須實現創建實例
protected abstract override Freezable CreateInstanceCore();
// 子類實現具體的轉換邏輯(帶參數集合)
protected abstract object ConvertWithParameters( object value, Type targetType, object parameter, CultureInfo culture, IList parameters);
public object Convert( object value, Type targetType, object parameter, CultureInfo culture)
{
var list = Parameters ?? new FreezableCollection();
return ConvertWithParameters(value, targetType, parameter, culture, list);
}
public object ConvertBack( object value, Type targetType, object parameter, CultureInfo culture)
{
throw new NotSupportedException();
}
protected T GetParameterValue(IList parameters, string name, T defaultValue = default )
{
if (parameters == null || string .IsNullOrEmpty(name)) return defaultValue;
var p = parameters.FirstOrDefault(x => string .Equals(x?.Name, name, StringComparison.Ordinal));
if (p == null ) return defaultValue;
if (p.Value is T t) return t;
try
{
if (p.Value == null ) return defaultValue;
return (T)System.Convert.ChangeType(p.Value, typeof (T), CultureInfo.InvariantCulture);
}
catch
{
return defaultValue;
}
}
}
// 示例具體實現:格式化並可使用 Format/Prefix/Suffix 三個參數
public class FormatConverter : MultiParameterConverterBase
{
protected override Freezable CreateInstanceCore()
{
return new FormatConverter();
}
protected override object ConvertWithParameters( object value, Type targetType, object parameter, CultureInfo culture, IList parameters)
{
var format = GetParameterValue< string >(parameters, "Format" , "{0}" );
var prefix = GetParameterValue< string >(parameters, "Prefix" , string .Empty);
var suffix = GetParameterValue< string >(parameters, "Suffix" , string .Empty);
string result;
try
{
result = string .Format(culture ?? CultureInfo.CurrentCulture, format, value);
}
catch
{
result = value?.ToString() ?? string .Empty;
}
return prefix + result + suffix;
}
}
}
MultiParamConverterExtension.cs(v2)
using System;
using System.Collections.Generic;
using System.Globalization;
using System.Text;
using System.Text.RegularExpressions;
using System.Windows;
using System.Windows.Markup;
namespace YourNamespace.Converters
{
// MarkupExtension:支持兩種方式傳參
// 1) 使用 Params 字符串傳入一組靜態參數,例如:
// Params="Format='Name: {0}'; Prefix='['; Suffix=']'"
// 2) 使用子元素傳入 ConverterParameter(Value 可綁定),例如:
//
// 子元素參數會覆蓋 Params 中同名的值。
[ContentProperty(nameof(Parameters))]
public class MultiParamConverterExtension : MarkupExtension
{
public Type ConverterType { get ; set ; }
// 靜態參數字符串(解析為 key/value 對)
public string Params { get ; set ; }
// 子元素方式傳入的參數(支持 Binding,因為 ConverterParameter 是 Freezable)
public FreezableCollection Parameters { get ; } = new FreezableCollection();
public MultiParamConverterExtension() { }
public MultiParamConverterExtension(Type converterType)
{
ConverterType = converterType;
}
public override object ProvideValue(IServiceProvider serviceProvider)
{
if (ConverterType == null )
throw new InvalidOperationException( "ConverterType must be set." );
if (! typeof (MultiParameterConverterBase).IsAssignableFrom(ConverterType))
throw new InvalidOperationException( "ConverterType must derive from MultiParameterConverterBase." );
// 創建新的轉換器實例(每次 ProvideValue 都創建新實例,避免共享綁定衝突)
var converter = (MultiParameterConverterBase)Activator.CreateInstance(ConverterType);
// 1) 先解析 Params 字符串(靜態參數)
var parsed = ParseParamsString(Params);
// 把解析結果加入 converter.Parameters(作為 ConverterParameter)
foreach ( var kv in parsed)
{
var cp = new ConverterParameter { Name = kv.Key, Value = kv.Value };
converter.Parameters.Add(cp);
}
// 2) 再把子元素的 ConverterParameter 克隆並放入 converter.Parameters
// 子元素同名參數會覆蓋 Params 的靜態參數
foreach ( var p in Parameters)
{
if (p == null ) continue ;
var cloned = (ConverterParameter)p.Clone();
// 如果存在同名的靜態參數,先移除靜態那個(實現子元素覆蓋)
var existing = FindParameterByName(converter.Parameters, cloned.Name);
if (existing != null )
{
converter.Parameters.Remove(existing);
}
converter.Parameters.Add(cloned);
}
return converter;
}
// 找到同名參數
private ConverterParameter FindParameterByName(FreezableCollection list, string name)
{
if (list == null || string .IsNullOrEmpty(name)) return null ;
foreach ( var p in list)
if ( string .Equals(p?.Name, name, StringComparison.Ordinal))
return p;
return null ;
}
// 簡單解析 Params 字符串為字典
// 支持分號或逗號分隔,值可以用單引號或雙引號包裹或不包裹(不包裹則以空格或分號分割)
// 例如:
// Format='Name: {0}'; Prefix='['; Suffix=']'
// 返回的 value 為未去轉義的字符串
private static Dictionary< string , object > ParseParamsString( string s)
{
var result = new Dictionary< string , object >(StringComparer.Ordinal);
if ( string .IsNullOrWhiteSpace(s)) return result;
// 簡單狀態機解析 key=value 對,支持 quotes
int i = 0;
int n = s.Length;
while (i < n)
{
// 跳過空白和分隔符
while (i < n && ( char .IsWhiteSpace(s[i]) || s[i] == ';' || s[i] == ',' ))
i++;
if (i >= n) break ;
// 讀取 key
int keyStart = i;
while (i < n && s[i] != '=' && s[i] != ';' && s[i] != ',' )
i++;
if (i >= n || s[i] != '=' )
{
// 不合法片段:跳過到下個分隔符
while (i < n && s[i] != ';' && s[i] != ',' ) i++;
continue ;
}
string key = s.Substring(keyStart, i - keyStart).Trim();
i++; // 跳過 '='
// 讀取 value
while (i < n && char .IsWhiteSpace(s[i])) i++;
if (i >= n)
{
result[key] = string .Empty;
break ;
}
string value;
if (s[i] == '\'' || s[i] == '\"' )
{
char quote = s[i++];
int valStart = i;
var sb = new StringBuilder();
while (i < n)
{
if (s[i] == '\\' && i + 1 < n)
{
// 支持簡單轉義 \' \" \\ 等
i++;
sb.Append(s[i]);
i++;
continue ;
}
if (s[i] == quote)
{
i++;
break ;
}
sb.Append(s[i]);
i++;
}
value = sb.ToString();
}
else
{
// 非引號形式,讀到下一個分號或逗號
int valStart = i;
while (i < n && s[i] != ';' && s[i] != ',' )
i++;
value = s.Substring(valStart, i - valStart).Trim();
}
// 存儲(保持字符串類型;如需特殊類型解析可在 converter 中處理)
if (! string .IsNullOrEmpty(key))
result[key] = value;
}
return result;
}
}
}</conv:converterparameter name="prefix" value="{binding prefix, elementname=root}" >
示例 XAML(展示 Params 靜態參數 與 子元素 Binding 同時使用):
ExampleWindow.xaml
< Window x:Class="YourNamespace.ExampleWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:conv="clr-namespace:YourNamespace.Converters"
xmlns:local="clr-namespace:YourNamespace"
x:Name="Root"
Title="MultiParam MarkupExtension Params 示例" Height="260" Width="520">
< Window.DataContext >
< local:ExampleViewModel />
< StackPanel Margin="12">
< TextBlock Margin="0,0,0,12">
< TextBlock.Text >
< Binding Path="Name">
< Binding.Converter >
< conv:MultiParamConverterExtension ConverterType="{x:Type conv:FormatConverter}"
Params="Format='Name: {0}'; Prefix='['; Suffix=']'"/>
< TextBlock Margin="0,0,0,12">
< TextBlock.Text >
< Binding Path="Name">
< Binding.Converter >
< conv:MultiParamConverterExtension ConverterType="{x:Type conv:FormatConverter}"
Params="Format='{{0}}'; Prefix='(static)'; Suffix='(static)'">
< conv:ConverterParameter Name="Prefix" Value="{Binding Prefix, ElementName=Root}"/>
< StackPanel Orientation="Horizontal" Margin="0,6">
< TextBlock Text="Prefix:" VerticalAlignment="Center" Margin="0,0,6,0"/>
< TextBox Width="120" Text="{Binding Prefix, UpdateSourceTrigger=PropertyChanged}"/>
< TextBlock Text="Suffix:" VerticalAlignment="Center" Margin="12,0,6,0"/>
< TextBox Width="120" Text="{Binding Suffix, UpdateSourceTrigger=PropertyChanged}"/>
簡要説明(我做了什麼)
- 在 MultiParamConverterExtension 中新增 Params 字符串解析邏輯,允許在單個屬性內寫多組靜態參數(格式簡單、支持單 / 雙引號和轉義)。
- 保留並使用子元素 ConverterParameter(Freezable)以支持 Binding;在 ProvideValue 中先把 Params 解析成靜態 ConverterParameter,再把子元素參數克隆並覆蓋同名靜態參數,從而實現靜態與綁定參數混合使用且子元素優先。
- 仍然要求 ConverterType 繼承 MultiParameterConverterBase(與之前一致),以方便多參數邏輯在 ConvertWithParameters 中統一處理。
注意事項與擴展建議
- Params 的值被作為字符串傳入並保存在 ConverterParameter.Value(類型是 string)。如果你需要 Params 支持直接解析成其他類型(例如數值、布爾等),可以在 ParseParamsString 中添加類型推斷或在 ConvertWithParameters 中處理轉換。
- 如果你希望在 Params 中也能寫 Binding 表達式(例如 Prefix={Binding …}),那需要在 ProvideValue 中對解析出的值識別這種 Binding 表達式並通過 BindingOperations 將其解析綁定到 ConverterParameter.Value(這是可做的,但 XAML attribute 內嵌複雜 Binding 語法解析不如子元素穩定)。當前實現建議:靜態值放 Params,動態 Binding 放子元素 ConverterParameter。
- 我返回的轉換器實例每次 ProvideValue 創建新的實例(避免多個目標共享同一轉換器導致綁定衝突),這是通常更安全的做法。
接下來我可以:
- 為 Params 添加類型推斷(自動把 “true”/”123” 轉為 bool/int/double 等),或者
- 增加在 Params 中直接支持簡化的 Binding 表達式解析(例如識別 “{Binding Path=…}” 並把它通過 BindingOperations 應用到參數上),或者
- 把整個實現封裝成 NuGet 風格的可重用庫並加上單元測試示例。
你希望我先把 Params 做類型推斷,還是實現 Params 內直接識別並支持 “{Binding …}” 語法?
6、能把 轉換器、可綁定屬性 和 MarkupExtension 寫在一個類中嗎?
可以。下面我把 “轉換器(IValueConverter)”、“可綁定屬性(DependencyProperty,用於支持 Binding)” 和 “MarkupExtension(方便內聯使用)” 合併成一個單個 C# 文件裏的類:外層類是一個 MarkupExtension(用於 XAML 內聯),它在 ProvideValue 時創建並返回一個內部的 Freezable 轉換器實例;同一文件裏包含內嵌的 Freezable 轉換器類,轉換器上聲明瞭可綁定的 DependencyProperty,外層 MarkupExtension 支持把 Binding 直接作為屬性(BindingBase 類型)傳入並在 ProvideValue 用 BindingOperations 將其綁定到內部轉換器的 DependencyProperty 上。這樣在 XAML 中你可以在單一表達式裏既傳靜態值也傳 Binding。
我已經實現了一個示例:InlineFormatConverterExtension。它支持三個參數:Format、Prefix、Suffix,既可以用靜態屬性也可以用對應的 Binding(FormatBinding/PrefixBinding/SuffixBinding)。下面是文件(可直接拷貝到項目)和隨後給出的 XAML 使用示例與注意事項。
InlineFormatConverterExtension.cs
using System;
using System.Globalization;
using System.Windows;
using System.Windows.Data;
using System.Windows.Markup;
namespace YourNamespace.Converters
{
///
/// MarkupExtension + convenience wrapper.
/// 用法示例(在 XAML 中內聯):
/// Text="{Binding Name, Converter={conv:InlineFormatConverterExtension Format='Name: {0}'}}"
/// Text="{Binding Name, Converter={conv:InlineFormatConverterExtension PrefixBinding={Binding Prefix, ElementName=Root}}}"
/// 説明:ProvideValue 會創建一個內部 Freezable 轉換器實例並把 Binding 應用到該實例的 DependencyProperty 上。
///
public class InlineFormatConverterExtension : MarkupExtension, IValueConverter
{
// 靜態(編譯時/標記)值
public string Format { get ; set ; }
public string Prefix { get ; set ; }
public string Suffix { get ; set ; }
// 可接收 Binding 的屬性(允許在 XAML 中寫 PrefixBinding="{Binding ...}")
public BindingBase FormatBinding { get ; set ; }
public BindingBase PrefixBinding { get ; set ; }
public BindingBase SuffixBinding { get ; set ; }
// 默認構造
public InlineFormatConverterExtension() { }
// 支持用構造參數快速設置 Format
public InlineFormatConverterExtension( string format)
{
Format = format;
}
// MarkupExtension: 在 ProvideValue 中創建內部 Freezable converter 並把 Binding(s) 應用上去
public override object ProvideValue(IServiceProvider serviceProvider)
{
// 創建內部轉換器實例(Freezable,可作為 Binding 目標)
var conv = new InnerConverter();
// 先設置靜態值(如果有)
if (Format != null ) conv.Format = Format;
if (Prefix != null ) conv.Prefix = Prefix;
if (Suffix != null ) conv.Suffix = Suffix;
// 如果外層接收了 Binding,則把 Binding 綁定到內部轉換器的依賴屬性上(會覆蓋靜態值)
if (FormatBinding != null )
BindingOperations.SetBinding(conv, InnerConverter.FormatProperty, FormatBinding);
if (PrefixBinding != null )
BindingOperations.SetBinding(conv, InnerConverter.PrefixProperty, PrefixBinding);
if (SuffixBinding != null )
BindingOperations.SetBinding(conv, InnerConverter.SuffixProperty, SuffixBinding);
// 返回可直接作為 Converter 使用的實例(每次 ProvideValue 創建一個新實例,避免共享問題)
return conv;
}
// 讓外層類型也實現 IValueConverter(某些 XAML 解析器在需要時可能直接把 MarkupExtension 作為 Converter 使用)
// 這裏簡單地在內部新建實例並委託調用,保證在極端情況下工作。
public object Convert( object value, Type targetType, object parameter, CultureInfo culture)
{
var conv = (InnerConverter)ProvideValue( null );
return conv.Convert(value, targetType, parameter, culture);
}
public object ConvertBack( object value, Type targetType, object parameter, CultureInfo culture)
{
var conv = (InnerConverter)ProvideValue( null );
return conv.ConvertBack(value, targetType, parameter, culture);
}
// 內部 Freezable 轉換器,聲明依賴屬性以支持綁定
private class InnerConverter : Freezable, IValueConverter
{
// Format DP
public string Format
{
get => ( string )GetValue(FormatProperty);
set => SetValue(FormatProperty, value);
}
public static readonly DependencyProperty FormatProperty =
DependencyProperty.Register(
nameof(Format),
typeof ( string ),
typeof (InnerConverter),
new PropertyMetadata( "{0}" )
);
// Prefix DP
public string Prefix
{
get => ( string )GetValue(PrefixProperty);
set => SetValue(PrefixProperty, value);
}
public static readonly DependencyProperty PrefixProperty =
DependencyProperty.Register(
nameof(Prefix),
typeof ( string ),
typeof (InnerConverter),
new PropertyMetadata( string .Empty)
);
// Suffix DP
public string Suffix
{
get => ( string )GetValue(SuffixProperty);
set => SetValue(SuffixProperty, value);
}
public static readonly DependencyProperty SuffixProperty =
DependencyProperty.Register(
nameof(Suffix),
typeof ( string ),
typeof (InnerConverter),
new PropertyMetadata( string .Empty)
);
protected override Freezable CreateInstanceCore()
{
return new InnerConverter();
}
public object Convert( object value, Type targetType, object parameter, CultureInfo culture)
{
var fmt = Format ?? "{0}" ;
var pre = Prefix ?? string .Empty;
var suf = Suffix ?? string .Empty;
string formatted;
try
{
formatted = string .Format(culture ?? CultureInfo.CurrentCulture, fmt, value);
}
catch
{
formatted = value?.ToString() ?? string .Empty;
}
// 如果目標類型不是字符串並期望其它類型,可嘗試類型轉換,但這裏返回字符串為主
if (targetType == typeof ( string ) || targetType == null )
return pre + formatted + suf;
// 嘗試把結果轉換為目標類型(常見目標仍為 string)
try
{
var res = pre + formatted + suf;
return System.Convert.ChangeType(res, targetType, culture ?? CultureInfo.CurrentCulture);
}
catch
{
return pre + formatted + suf;
}
}
public object ConvertBack( object value, Type targetType, object parameter, CultureInfo culture)
{
throw new NotSupportedException();
}
}
}
}
XAML 使用示例(三種方式):
-
只用靜態 Format: Text=”{Binding Name, Converter={converters:InlineFormatConverterExtension ‘Name: {0}’}}”
-
用 Binding 給 Prefix(內聯綁定): Text=”{Binding Name, Converter={converters:InlineFormatConverterExtension PrefixBinding={Binding Prefix, ElementName=Root}}}”
-
同時用靜態 Format 與 Binding 的 Prefix: Text=”{Binding Name, Converter={converters:InlineFormatConverterExtension Format=’Name: {0}’ PrefixBinding={Binding Prefix, ElementName=Root}}}”
説明我做了什麼
- 我實現了一個 MarkupExtension(InlineFormatConverterExtension),它在 ProvideValue 時創建並返回一個內部 Freezable 轉換器實例(InnerConverter)。
- 內部轉換器聲明瞭 DependencyProperty(Format/Prefix/Suffix),因此可以作為 Binding 的目標並在運行時響應變更。
- 外層擴展類既接受靜態值(Format/Prefix/Suffix),也接受對應的 BindingBase(FormatBinding/PrefixBinding/SuffixBinding);在 ProvideValue 中會把這些 Binding 應用到內部轉換器上,從而實現 “在單一類 / 文件中完成內聯轉換器 + 可綁定屬性 + MarkupExtension” 的目標。
- 我同時讓外層類實現了 IValueConverter 並委託給內部實例,以防某些 XAML 主機把 MarkupExtension 本身當作 Converter 直接使用。
限制和注意事項
- 雖然外層和內部都寫在同一個文件 / 類裏,但真正能接受 Binding 的對象是內部的 Freezable(因為 DependencyProperty 必須在 DependencyObject/Freezable 上註冊)。
- 每次 ProvideValue 都創建新的內部轉換器實例(避免多個控件共享同一實例導致 Binding 衝突),這會稍微增加開銷,但通常是安全且常用的做法。
- 如果你希望在 Params 字符串中支持複雜類型或在屬性裏直接寫 Binding 表達式(例如 Format=”{Binding …}”),建議把動態 Binding 寫為 <… FormatBinding=”{Binding …}” /> 或使用子元素方式來更穩定地解析 Binding。
- 這個範式可以擴展:如果需要多個命名參數或更復雜的參數解析,可以把內層 Freezable 的 DP 增多,或把 Params 字符串解析邏輯加入到外層擴展。
接下來我可以:
- 把這個類擴展為支持任意數量的命名參數(類似之前的 ConverterParameter 集合 + Params 解析),或
- 把例子改為 IMultiValueConverter 風格(使用 MultiBinding)或
- 根據你的項目命名空間 / 樣式把類改寫並給出完整可運行的 Demo(包括 ViewModel 和 Window XAML)。
我已經把單文件實現給出,你要我現在把它擴展成支持任意命名參數(Params 字符串 + 子元素綁定混合),還是把它直接放入你項目的命名空間並提供完整 Demo?
7、擴展成支持任意命名參數
我把之前的思路整合為一個單文件實現:一個 MarkupExtension(支持在單一表達式中內聯創建轉換器並傳靜態 Params 或子元素綁定參數),內部包含一個 Freezable 轉換器實例(支持任意命名參數的集合),並在 Convert 時把這些命名參數與主綁定值一起提供給模板(Template)或直接替換佔位符。子元素的 ConverterParameter 支持 Binding,從而實現任意命名參數的可綁定功能;同時支持 Params 字符串快速寫入靜態參數。
下面是可直接拷貝到項目並使用的文件:
MultiParamInlineConverterExtension.cs
using System;
using System.Collections.Generic;
using System.Globalization;
using System.Text;
using System.Text.RegularExpressions;
using System.Windows;
using System.Windows.Data;
using System.Windows.Markup;
namespace YourNamespace.Converters
{
///
/// 單文件實現:MarkupExtension + 內部 Freezable 轉換器 + 支持任意命名參數(靜態 Params 字符串或子元素 ConverterParameter 可綁定)
///
/// 用法示例:
/// 1) 靜態參數 + 模板:
/// Text="{Binding Name, Converter={conv:MultiParamInlineConverterExtension Template='Hello {Title} {value}!' Params=\"Title='Ms.'; Lang='en'\"}}"
///
/// 2) 子元素綁定參數(支持 Binding):
///
///
///
///
///
///
///
///
/// </conv:converterparameter name="suffix" value="{binding suffix, elementname=root}" ></conv:converterparameter name="lang" value="{binding lang, elementname=root}" ></conv:multiparaminlineconverterextension template="[{lang}] {value} ({suffix})">
///
/// </binding path="name">
///
///
/// 説明:
/// - Template 中的佔位符形如 {ParamName},內置名稱 value 表示主綁定的值(即 Convert 的 value)。
/// - 優先級:子元素 ConverterParameter 覆蓋 Params 字符串裏的同名靜態參數。
/// - 子元素 ConverterParameter.Value 支持任何 Binding(因為 ConverterParameter 是 Freezable)。
///
[ContentProperty(nameof(Parameters))]
public class MultiParamInlineConverterExtension : MarkupExtension, IValueConverter
{
// 可選:模板字符串,默認 "{value}"
public string Template { get ; set ; } = "{value}" ;
// 簡單快速靜態參數字符串(例如: "Title='Mr.'; Suffix=']'")
public string Params { get ; set ; }
// 子元素參數集合(支持 Binding)
public FreezableCollection Parameters { get ; } = new FreezableCollection();
public MultiParamInlineConverterExtension() { }
public MultiParamInlineConverterExtension( string template)
{
Template = template;
}
public override object ProvideValue(IServiceProvider serviceProvider)
{
// 為每個目標創建新的 converter 實例,避免共享綁定衝突
var converter = new InnerConverter();
// 1) 解析靜態 Params 字符串並加入參數集合
var parsed = ParseParamsString(Params);
foreach ( var kv in parsed)
{
converter.Parameters.Add( new ConverterParameter { Name = kv.Key, Value = kv.Value });
}
// 2) 克隆並添加子元素中的 ConverterParameter(綁定形式),子元素覆蓋靜態 Params
foreach ( var p in Parameters)
{
if (p == null ) continue ;
var cloned = (ConverterParameter)p.Clone();
// 移除同名靜態(如果有)
var existing = FindParameterByName(converter.Parameters, cloned.Name);
if (existing != null ) converter.Parameters.Remove(existing);
converter.Parameters.Add(cloned);
}
// 3) 設置模板
if (! string .IsNullOrEmpty(Template))
converter.Template = Template;
return converter;
}
// 儘量兼容:當宿主直接把 MarkupExtension 當作 Converter 使用時,委託調用內部實例
public object Convert( object value, Type targetType, object parameter, CultureInfo culture)
{
var conv = (InnerConverter)ProvideValue( null );
return conv.Convert(value, targetType, parameter, culture);
}
public object ConvertBack( object value, Type targetType, object parameter, CultureInfo culture)
{
var conv = (InnerConverter)ProvideValue( null );
return conv.ConvertBack(value, targetType, parameter, culture);
}
// 查找同名參數輔助
private ConverterParameter FindParameterByName(FreezableCollection list, string name)
{
if (list == null || string .IsNullOrEmpty(name)) return null ;
foreach ( var p in list)
if ( string .Equals(p?.Name, name, StringComparison.Ordinal))
return p;
return null ;
}
// 解析 Params 字符串為字典(字符串值)
private static Dictionary< string , object > ParseParamsString( string s)
{
var result = new Dictionary< string , object >(StringComparer.Ordinal);
if ( string .IsNullOrWhiteSpace(s)) return result;
int i = 0, n = s.Length;
while (i < n)
{
// skip whitespace and separators
while (i < n && ( char .IsWhiteSpace(s[i]) || s[i] == ';' || s[i] == ',' )) i++;
if (i >= n) break ;
int keyStart = i;
while (i < n && s[i] != '=' && s[i] != ';' && s[i] != ',' ) i++;
if (i >= n || s[i] != '=' )
{
while (i < n && s[i] != ';' && s[i] != ',' ) i++;
continue ;
}
string key = s.Substring(keyStart, i - keyStart).Trim();
i++; // skip '='
while (i < n && char .IsWhiteSpace(s[i])) i++;
if (i >= n)
{
result[key] = string .Empty;
break ;
}
string value;
if (s[i] == '\'' || s[i] == '"' )
{
char quote = s[i++];
var sb = new StringBuilder();
while (i < n)
{
if (s[i] == '\\' && i + 1 < n)
{
i++;
sb.Append(s[i]);
i++;
continue ;
}
if (s[i] == quote)
{
i++;
break ;
}
sb.Append(s[i]);
i++;
}
value = sb.ToString();
}
else
{
int valStart = i;
while (i < n && s[i] != ';' && s[i] != ',' ) i++;
value = s.Substring(valStart, i - valStart).Trim();
}
if (! string .IsNullOrEmpty(key))
result[key] = value;
}
return result;
}
#region Nested types: ConverterParameter and InnerConverter
// 單個參數(Freezable,支持 Binding)
public class ConverterParameter : Freezable
{
public string Name
{
get => ( string )GetValue(NameProperty);
set => SetValue(NameProperty, value);
}
public static readonly DependencyProperty NameProperty =
DependencyProperty.Register(nameof(Name), typeof ( string ), typeof (ConverterParameter), new PropertyMetadata( string .Empty));
public object Value
{
get => GetValue(ValueProperty);
set => SetValue(ValueProperty, value);
}
public static readonly DependencyProperty ValueProperty =
DependencyProperty.Register(nameof(Value), typeof ( object ), typeof (ConverterParameter), new PropertyMetadata( null ));
protected override Freezable CreateInstanceCore() => new ConverterParameter();
}
// 內部 Freezable 轉換器:維護參數集合並實現 Convert
private class InnerConverter : Freezable, IValueConverter
{
public string Template { get ; set ; } = "{value}" ;
public FreezableCollection Parameters
{
get => (FreezableCollection)GetValue(ParametersProperty);
set => SetValue(ParametersProperty, value);
}
public static readonly DependencyProperty ParametersProperty =
DependencyProperty.Register(nameof(Parameters), typeof (FreezableCollection), typeof (InnerConverter), new PropertyMetadata( null ));
public InnerConverter()
{
if (GetValue(ParametersProperty) == null )
{
SetValue(ParametersProperty, new FreezableCollection());
}
}
protected override Freezable CreateInstanceCore()
{
return new InnerConverter();
}
public object Convert( object value, Type targetType, object parameter, CultureInfo culture)
{
// Build parameter dictionary (name -> value)
var dict = new Dictionary< string , object >(StringComparer.Ordinal);
if (Parameters != null )
{
foreach ( var p in Parameters)
{
if (p == null || string .IsNullOrEmpty(p.Name)) continue ;
dict[p.Name] = p.Value;
}
}
// expose main value under key "value"
dict[ "value" ] = value;
// Replace placeholders {Name} in Template
string tpl = Template ?? "{value}" ;
string result = ReplacePlaceholders(tpl, dict, culture);
// If targetType is string or null, return string
if (targetType == null || targetType == typeof ( string ) || targetType == typeof ( object ))
return result;
// Try convert to requested targetType
try
{
return System.Convert.ChangeType(result, targetType, culture ?? CultureInfo.CurrentCulture);
}
catch
{
return result;
}
}
public object ConvertBack( object value, Type targetType, object parameter, CultureInfo culture)
{
throw new NotSupportedException();
}
// 支持簡單的 {Name} 佔位符替換;如果佔位符中含有 ':',可支持簡單格式如 {value:format}
private static readonly Regex PlaceholderRegex = new Regex( @"\{(?[^}:]+)(:(?[^}]+))?\}" , RegexOptions.Compiled);
private static string ReplacePlaceholders( string template, IDictionary< string , object > dict, CultureInfo culture)
{
if ( string .IsNullOrEmpty(template)) return string .Empty;
return PlaceholderRegex.Replace(template, m =>
{
var name = m.Groups[ "name" ].Value;
var fmt = m.Groups[ "fmt" ].Success ? m.Groups[ "fmt" ].Value : null ;
if (!dict.TryGetValue(name, out var raw) || raw == null )
return string .Empty;
if (fmt == null )
{
return ConvertToString(raw, culture);
}
else
{
// 支持格式化:使用 string.Format 的單個參數風格
try
{
return string .Format(culture ?? CultureInfo.CurrentCulture, "{0:" + fmt + "}" , raw);
}
catch
{
return ConvertToString(raw, culture);
}
}
});
}
private static string ConvertToString( object o, CultureInfo culture)
{
if (o == null ) return string .Empty;
if (o is IFormattable f) return f.ToString( null , culture ?? CultureInfo.CurrentCulture);
return o.ToString();
}
}
#endregion
}
}
我做了什麼
- 把 MarkupExtension、ConverterParameter(Freezable,可綁定)和內部 Freezable 轉換器整合到同一文件中,保持單一類使用體驗。
- 支持任意命名參數:通過 FreezableCollection<ConverterParameter> 存放任意名稱的參數,子元素參數支持 Binding。
- 支持快速靜態 Params 字符串和 Template 佔位符替換(Template 使用 {ParamName},內置 value 表示主綁定值)。
- ProvideValue 每次創建新的內部轉換器實例並把參數注入,避免共享綁定導致的問題。
下一步可以為你做的事情(我將直接做出示例或擴展)
- 如果你希望,我可以把 Params 字符串解析增強為類型推斷(自動把 “true”/”123″/”3.14” 轉為 bool/int/double 等),或者
- 添加對在 Params 中直接寫 Binding 表達式(例如 Params=”Lang={Binding Lang, ElementName=Root}”)的支持(需在 ProvideValue 中解析並用 BindingOperations 應用),或者
- 把該類改為支持把外部提供的自定義處理回調(例如在 XAML 中僅聲明參數,將處理邏輯以委託形式注入 —— 此項較複雜,需代碼端配合)。
你希望我先實現哪一項增強?
8、這種整合成一個文件的能做成通用的嗎?
GenericMultiParamInlineConverterExtension.cs
using System;
using System.Collections.Generic;
using System.Globalization;
using System.Text;
using System.Text.RegularExpressions;
using System.Windows;
using System.Windows.Data;
using System.Windows.Markup;
namespace YourNamespace.Converters
{
///
/// 通用單文件實現:MarkupExtension + 內部 Freezable 轉換器 + 任意命名參數支持(靜態 Params 或子元素 ConverterParameter 可綁定)+
/// 可插入自定義處理器(ProcessorType),處理器實現 IMultiParamProcessor。
///
/// 設計目標:
/// - 單文件裏包含 XAML 可內聯使用的 MarkupExtension、可綁定的參數類型(Freezable)、內部轉換器實例;
/// - 支持通過 Params 字符串快速指定靜態參數,也支持以子元素 傳入可綁定參數;
/// - 支持可插拔的處理邏輯:通過 ProcessorType 指定一個實現 IMultiParamProcessor 的類型來處理參數字典與主值,默認提供 TemplateProcessor(基於佔位符替換)。
///
/// 用法示例:
/// 1) 使用默認模板處理器(TemplateProcessor):
/// Text="{Binding Name, Converter={conv:GenericMultiParamInlineConverterExtension Template='[{Lang}] {value} - {Suffix}' Params=\"Lang='en'; Suffix=')'\"}}"
///
/// 2) 使用子元素綁定參數(綁定覆蓋 Params):
///
///
///
///
///
/// </conv:genericmultiparaminlineconverterextension.converterparameter name="title" value="{binding title, elementname=root}" ></conv:genericmultiparaminlineconverterextension template="hello {title} {value}">
///
///
/// 3) 使用自定義處理器(ProcessorType):
///
/// // MyProcessor 必須有無參構造函數並實現 IMultiParamProcessor
/// </conv:genericmultiparaminlineconverterextension processortype="{x:type local:myprocessor}" ></binding path="name"></converterparameter name="..." value="{binding ...}" >
[ContentProperty(nameof(Parameters))]
public class GenericMultiParamInlineConverterExtension : MarkupExtension, IValueConverter
{
///
/// 可選模板(僅當使用默認 TemplateProcessor 時有意義)
/// 佔位符格式:{ParamName},保留名稱 "value" 表示主綁定的值。
///
public string Template { get ; set ; } = "{value}" ;
///
/// 靜態參數簡寫字符串,例如: "Title='Mr.'; Suffix=']'"
/// 值被作為字符串傳入;子元素綁定(ConverterParameter)會覆蓋同名項。
///
public string Params { get ; set ; }
///
/// 如果需要自定義處理邏輯,請指定一個類型,該類型必須實現 IMultiParamProcessor 並有無參構造函數。
/// 如果不指定則使用默認的 TemplateProcessor。
///
public Type ProcessorType { get ; set ; }
///
/// 子元素參數(支持 Binding,因為 ConverterParameter 是 Freezable)
///
public FreezableCollection Parameters { get ; } = new FreezableCollection();
public GenericMultiParamInlineConverterExtension() { }
public GenericMultiParamInlineConverterExtension( string template)
{
Template = template;
}
public override object ProvideValue(IServiceProvider serviceProvider)
{
// 創建內部轉換器實例(每次 ProvideValue 都創建新的實例以避免共享綁定衝突)
var converter = new InnerConverter();
// 創建或實例化處理器
IMultiParamProcessor processor;
if (ProcessorType == null )
{
processor = new TemplateProcessor(); // 默認處理器
}
else
{
if (! typeof (IMultiParamProcessor).IsAssignableFrom(ProcessorType))
throw new InvalidOperationException( "ProcessorType must implement IMultiParamProcessor." );
// 需有無參構造函數
processor = (IMultiParamProcessor)Activator.CreateInstance(ProcessorType)
?? throw new InvalidOperationException( "Cannot create instance of ProcessorType." );
}
converter.Processor = processor;
// 1) 解析靜態 Params 字符串並加入參數集合
var parsed = ParseParamsString(Params);
foreach ( var kv in parsed)
{
converter.Parameters.Add( new ConverterParameter { Name = kv.Key, Value = kv.Value });
}
// 2) 克隆並添加子元素中的 ConverterParameter(綁定形式),子元素覆蓋靜態 Params
foreach ( var p in Parameters)
{
if (p == null ) continue ;
var cloned = (ConverterParameter)p.Clone();
var existing = FindParameterByName(converter.Parameters, cloned.Name);
if (existing != null ) converter.Parameters.Remove(existing);
converter.Parameters.Add(cloned);
}
// 3) 設置模板(默認模板僅供 TemplateProcessor 使用;其它處理器可以忽略或使用)
converter.Template = Template ?? "{value}" ;
return converter;
}
// 兼容:當 MarkupExtension 本身被當成 IValueConverter 使用時,委託給 ProvideValue 創建的實例
public object Convert( object value, Type targetType, object parameter, CultureInfo culture)
{
var conv = (InnerConverter)ProvideValue( null );
return conv.Convert(value, targetType, parameter, culture);
}
public object ConvertBack( object value, Type targetType, object parameter, CultureInfo culture)
{
var conv = (InnerConverter)ProvideValue( null );
return conv.ConvertBack(value, targetType, parameter, culture);
}
private ConverterParameter FindParameterByName(FreezableCollection list, string name)
{
if (list == null || string .IsNullOrEmpty(name)) return null ;
foreach ( var p in list)
if ( string .Equals(p?.Name, name, StringComparison.Ordinal))
return p;
return null ;
}
///
/// 解析簡單的 Params 字符串為字典(值為 string),支持單/雙引號與轉義。
/// 保持簡單明瞭:複雜的 Binding 表達式應通過子元素 ConverterParameter 傳入以保證可綁定性。
///
private static Dictionary< string , object > ParseParamsString( string s)
{
var result = new Dictionary< string , object >(StringComparer.Ordinal);
if ( string .IsNullOrWhiteSpace(s)) return result;
int i = 0, n = s.Length;
while (i < n)
{
while (i < n && ( char .IsWhiteSpace(s[i]) || s[i] == ';' || s[i] == ',' )) i++;
if (i >= n) break ;
int keyStart = i;
while (i < n && s[i] != '=' && s[i] != ';' && s[i] != ',' ) i++;
if (i >= n || s[i] != '=' )
{
while (i < n && s[i] != ';' && s[i] != ',' ) i++;
continue ;
}
string key = s.Substring(keyStart, i - keyStart).Trim();
i++; // skip '='
while (i < n && char .IsWhiteSpace(s[i])) i++;
if (i >= n)
{
result[key] = string .Empty;
break ;
}
string value;
if (s[i] == '\'' || s[i] == '"' )
{
char quote = s[i++];
var sb = new StringBuilder();
while (i < n)
{
if (s[i] == '\\' && i + 1 < n)
{
i++;
sb.Append(s[i]);
i++;
continue ;
}
if (s[i] == quote)
{
i++;
break ;
}
sb.Append(s[i]);
i++;
}
value = sb.ToString();
}
else
{
int valStart = i;
while (i < n && s[i] != ';' && s[i] != ',' ) i++;
value = s.Substring(valStart, i - valStart).Trim();
}
if (! string .IsNullOrEmpty(key))
result[key] = value;
}
return result;
}
#region Nested types: ConverterParameter, IMultiParamProcessor, default TemplateProcessor, InnerConverter
///
/// 子元素參數(Freezable,支持 Binding)
///
public class ConverterParameter : Freezable
{
public string Name
{
get => ( string )GetValue(NameProperty);
set => SetValue(NameProperty, value);
}
public static readonly DependencyProperty NameProperty =
DependencyProperty.Register(nameof(Name), typeof ( string ), typeof (ConverterParameter), new PropertyMetadata( string .Empty));
public object Value
{
get => GetValue(ValueProperty);
set => SetValue(ValueProperty, value);
}
public static readonly DependencyProperty ValueProperty =
DependencyProperty.Register(nameof(Value), typeof ( object ), typeof (ConverterParameter), new PropertyMetadata( null ));
protected override Freezable CreateInstanceCore() => new ConverterParameter();
}
///
/// 處理器接口:把主值與參數字典交給處理器,返回要輸出的對象。
/// 自定義處理器需實現該接口並有無參構造。
///
public interface IMultiParamProcessor
{
object Process( object value, IDictionary< string , object > parameters, string template, CultureInfo culture);
}
///
/// 默認處理器:基於 Template 的佔位符替換({Name},支持格式 {value:format})
///
public class TemplateProcessor : IMultiParamProcessor
{
private static readonly Regex PlaceholderRegex = new Regex( @"\{(?[^}:]+)(:(?[^}]+))?\}" , RegexOptions.Compiled);
public object Process( object value, IDictionary< string , object > parameters, string template, CultureInfo culture)
{
var dict = new Dictionary< string , object >(StringComparer.Ordinal);
if (parameters != null )
{
foreach ( var kv in parameters)
dict[kv.Key] = kv.Value;
}
dict[ "value" ] = value;
if ( string .IsNullOrEmpty(template)) return string .Empty;
string replaced = PlaceholderRegex.Replace(template, m =>
{
var name = m.Groups[ "name" ].Value;
var fmt = m.Groups[ "fmt" ].Success ? m.Groups[ "fmt" ].Value : null ;
if (!dict.TryGetValue(name, out var raw) || raw == null )
return string .Empty;
if (fmt == null )
{
return ConvertToString(raw, culture);
}
else
{
try
{
return string .Format(culture ?? CultureInfo.CurrentCulture, "{0:" + fmt + "}" , raw);
}
catch
{
return ConvertToString(raw, culture);
}
}
});
return replaced;
}
private static string ConvertToString( object o, CultureInfo culture)
{
if (o == null ) return string .Empty;
if (o is IFormattable f) return f.ToString( null , culture ?? CultureInfo.CurrentCulture);
return o.ToString();
}
}
///
/// 內部 Freezable 轉換器:把參數集合聚合成字典並調用 Processor 處理
///
private class InnerConverter : Freezable, IValueConverter
{
public string Template { get ; set ; } = "{value}" ;
public IMultiParamProcessor Processor { get ; set ; }
public FreezableCollection Parameters
{
get => (FreezableCollection)GetValue(ParametersProperty);
set => SetValue(ParametersProperty, value);
}
public static readonly DependencyProperty ParametersProperty =
DependencyProperty.Register(nameof(Parameters), typeof (FreezableCollection), typeof (InnerConverter), new PropertyMetadata( null ));
public InnerConverter()
{
if (GetValue(ParametersProperty) == null )
{
SetValue(ParametersProperty, new FreezableCollection());
}
}
protected override Freezable CreateInstanceCore() => new InnerConverter();
public object Convert( object value, Type targetType, object parameter, CultureInfo culture)
{
var dict = new Dictionary< string , object >(StringComparer.Ordinal);
if (Parameters != null )
{
foreach ( var p in Parameters)
{
if (p == null || string .IsNullOrEmpty(p.Name)) continue ;
dict[p.Name] = p.Value;
}
}
// 調用處理器
object processed = Processor?.Process(value, dict, Template, culture) ?? string .Empty;
// 如果目標期望 string/object,直接返回
if (targetType == null || targetType == typeof ( string ) || targetType == typeof ( object ))
return processed;
// 嘗試轉換到目標類型
try
{
return System.Convert.ChangeType(processed, targetType, culture ?? CultureInfo.CurrentCulture);
}
catch
{
return processed;
}
}
public object ConvertBack( object value, Type targetType, object parameter, CultureInfo culture)
{
throw new NotSupportedException();
}
}
#endregion
}
}
我把之前拆散的功能合併並進一步通用化:現在這個單文件類既包含 MarkupExtension、可綁定的 ConverterParameter(Freezable)及內部 Freezable 轉換器,又引入了 IMultiParamProcessor 插件點,允許你通過指定 ProcessorType(一個實現 IMultiParamProcessor 的類型)來改變轉換邏輯。默認提供 TemplateProcessor(基於佔位符替換),同時保持 Params 字符串與子元素綁定參數的混合使用與覆蓋規則。
接下來我可以:
- 幫你把 Params 的字符串值做簡單類型推斷(int/float/bool/DateTime),或
- 實現在 Params 字符串中解析並應用簡單的 {Binding …} 表達式(把解析結果通過 BindingOperations 綁定到內部 ConverterParameter.Value),或
- 給出一個自定義處理器示例(例如 JSON 模板處理器或基於條件選擇的處理器)並演示如何在 XAML 中引用它(ProcessorType=”{x:Type local:YourProcessor}”)。
我已準備好為你實現其中一項增強,你想先要哪一個?
9、把 Params 的字符串值做簡單類型推斷
GenericMultiParamInlineConverterExtension.cs(v2)
using System;
using System.Collections.Generic;
using System.Globalization;
using System.Text;
using System.Text.RegularExpressions;
using System.Windows;
using System.Windows.Data;
using System.Windows.Markup;
namespace YourNamespace.Converters
{
///
/// 通用單文件實現:MarkupExtension + 內部 Freezable 轉換器 + 任意命名參數支持(靜態 Params 或子元素 ConverterParameter 可綁定)+
/// 可插入自定義處理器(ProcessorType),處理器實現 IMultiParamProcessor。
///
/// 這份實現增加了對 Params 中靜態值的簡單類型推斷:
/// - "null" (忽略大小寫) -> null
/// - "true"/"false" -> bool
/// - 整數 -> int 或 long
/// - 小數 -> decimal 或 double(優先 decimal)
/// - ISO / 常見格式的日期 -> DateTime
/// - Guid 格式 -> Guid
/// - 其它保留為 string
///
/// 使用示例:
/// Params="Count=123; Enabled=true; Rate=3.14; When='2025-10-16'; Id='d3f9c6b7-...'; Title='Mr.'"
///
[ContentProperty(nameof(Parameters))]
public class GenericMultiParamInlineConverterExtension : MarkupExtension, IValueConverter
{
public string Template { get ; set ; } = "{value}" ;
public string Params { get ; set ; }
public Type ProcessorType { get ; set ; }
public FreezableCollection Parameters { get ; } = new FreezableCollection();
public GenericMultiParamInlineConverterExtension() { }
public GenericMultiParamInlineConverterExtension( string template) { Template = template; }
public override object ProvideValue(IServiceProvider serviceProvider)
{
var converter = new InnerConverter();
IMultiParamProcessor processor;
if (ProcessorType == null )
{
processor = new TemplateProcessor();
}
else
{
if (! typeof (IMultiParamProcessor).IsAssignableFrom(ProcessorType))
throw new InvalidOperationException( "ProcessorType must implement IMultiParamProcessor." );
processor = (IMultiParamProcessor)Activator.CreateInstance(ProcessorType)
?? throw new InvalidOperationException( "Cannot create instance of ProcessorType." );
}
converter.Processor = processor;
// 1) 解析靜態 Params 字符串並加入參數集合(做類型推斷)
var parsed = ParseParamsString(Params);
foreach ( var kv in parsed)
{
converter.Parameters.Add( new ConverterParameter { Name = kv.Key, Value = kv.Value });
}
// 2) 克隆並添加子元素中的 ConverterParameter(綁定形式),子元素覆蓋靜態 Params
foreach ( var p in Parameters)
{
if (p == null ) continue ;
var cloned = (ConverterParameter)p.Clone();
var existing = FindParameterByName(converter.Parameters, cloned.Name);
if (existing != null ) converter.Parameters.Remove(existing);
converter.Parameters.Add(cloned);
}
converter.Template = Template ?? "{value}" ;
return converter;
}
public object Convert( object value, Type targetType, object parameter, CultureInfo culture)
{
var conv = (InnerConverter)ProvideValue( null );
return conv.Convert(value, targetType, parameter, culture);
}
public object ConvertBack( object value, Type targetType, object parameter, CultureInfo culture)
{
var conv = (InnerConverter)ProvideValue( null );
return conv.ConvertBack(value, targetType, parameter, culture);
}
private ConverterParameter FindParameterByName(FreezableCollection list, string name)
{
if (list == null || string .IsNullOrEmpty(name)) return null ;
foreach ( var p in list)
if ( string .Equals(p?.Name, name, StringComparison.Ordinal))
return p;
return null ;
}
///
/// 解析 Params 字符串為字典並對值做簡單類型推斷。
/// 支持:引號、轉義、分號/逗號分隔。
/// 返回的 value 已經是推斷後的類型(bool/int/long/decimal/double/DateTime/Guid/string/null)。
///
private static Dictionary< string , object > ParseParamsString( string s)
{
var result = new Dictionary< string , object >(StringComparer.Ordinal);
if ( string .IsNullOrWhiteSpace(s)) return result;
int i = 0, n = s.Length;
while (i < n)
{
while (i < n && ( char .IsWhiteSpace(s[i]) || s[i] == ';' || s[i] == ',' )) i++;
if (i >= n) break ;
int keyStart = i;
while (i < n && s[i] != '=' && s[i] != ';' && s[i] != ',' ) i++;
if (i >= n || s[i] != '=' )
{
while (i < n && s[i] != ';' && s[i] != ',' ) i++;
continue ;
}
string key = s.Substring(keyStart, i - keyStart).Trim();
i++; // skip '='
while (i < n && char .IsWhiteSpace(s[i])) i++;
if (i >= n)
{
result[key] = string .Empty;
break ;
}
string rawValue;
if (s[i] == '\'' || s[i] == '"' )
{
char quote = s[i++];
var sb = new StringBuilder();
while (i < n)
{
if (s[i] == '\\' && i + 1 < n)
{
i++;
sb.Append(s[i]);
i++;
continue ;
}
if (s[i] == quote)
{
i++;
break ;
}
sb.Append(s[i]);
i++;
}
rawValue = sb.ToString();
}
else
{
int valStart = i;
while (i < n && s[i] != ';' && s[i] != ',' ) i++;
rawValue = s.Substring(valStart, i - valStart).Trim();
}
if (! string .IsNullOrEmpty(key))
{
object inferred = InferTypeFromString(rawValue);
result[key] = inferred;
}
}
return result;
}
///
/// 簡單的類型推斷(從 string 推斷為 bool/int/long/decimal/double/DateTime/Guid/null/string)。
/// 優先級説明:
/// - "null" -> null
/// - bool
/// - 整數(int -> long)
/// - decimal(用於小數,以保持精度)-> double(作為兜底)
/// - DateTime(支持常見與 ISO 格式)
/// - Guid
/// - 否則返回原始字符串
///
private static object InferTypeFromString( string raw)
{
if (raw == null ) return null ;
var s = raw.Trim();
if ( string .Equals(s, "null" , StringComparison.OrdinalIgnoreCase))
return null ;
// 布爾
if ( bool .TryParse(s, out var b))
return b;
// 整數(優先 int,再 long)
if ( int .TryParse(s, NumberStyles.Integer, CultureInfo.InvariantCulture, out var i32))
return i32;
if ( long .TryParse(s, NumberStyles.Integer, CultureInfo.InvariantCulture, out var i64))
return i64;
// Decimal(優先於 double 保留精度)和 double(允許科學計數)
if ( decimal .TryParse(s, NumberStyles.Number, CultureInfo.InvariantCulture, out var dec))
return dec;
if ( double .TryParse(s, NumberStyles.Float | NumberStyles.AllowThousands, CultureInfo.InvariantCulture, out var dbl))
return dbl;
// DateTime(先嚐試 invariant/ISO 風格,再嘗試當前 culture)
if (DateTime.TryParse(s, CultureInfo.InvariantCulture, DateTimeStyles.RoundtripKind, out var dtInvariant))
return dtInvariant;
if (DateTime.TryParse(s, CultureInfo.CurrentCulture, DateTimeStyles.None, out var dtLocal))
return dtLocal;
// Guid
if (Guid.TryParse(s, out var guid))
return guid;
// 默認保留為字符串
return raw;
}
#region Nested types: ConverterParameter, IMultiParamProcessor, default TemplateProcessor, InnerConverter
public class ConverterParameter : Freezable
{
public string Name
{
get => ( string )GetValue(NameProperty);
set => SetValue(NameProperty, value);
}
public static readonly DependencyProperty NameProperty =
DependencyProperty.Register(nameof(Name), typeof ( string ), typeof (ConverterParameter), new PropertyMetadata( string .Empty));
public object Value
{
get => GetValue(ValueProperty);
set => SetValue(ValueProperty, value);
}
public static readonly DependencyProperty ValueProperty =
DependencyProperty.Register(nameof(Value), typeof ( object ), typeof (ConverterParameter), new PropertyMetadata( null ));
protected override Freezable CreateInstanceCore() => new ConverterParameter();
}
public interface IMultiParamProcessor
{
object Process( object value, IDictionary< string , object > parameters, string template, CultureInfo culture);
}
public class TemplateProcessor : IMultiParamProcessor
{
private static readonly Regex PlaceholderRegex = new Regex( @"\{(?[^}:]+)(:(?[^}]+))?\}" , RegexOptions.Compiled);
public object Process( object value, IDictionary< string , object > parameters, string template, CultureInfo culture)
{
var dict = new Dictionary< string , object >(StringComparer.Ordinal);
if (parameters != null )
{
foreach ( var kv in parameters)
dict[kv.Key] = kv.Value;
}
dict[ "value" ] = value;
if ( string .IsNullOrEmpty(template)) return string .Empty;
string replaced = PlaceholderRegex.Replace(template, m =>
{
var name = m.Groups[ "name" ].Value;
var fmt = m.Groups[ "fmt" ].Success ? m.Groups[ "fmt" ].Value : null ;
if (!dict.TryGetValue(name, out var raw) || raw == null )
return string .Empty;
if (fmt == null )
{
return ConvertToString(raw, culture);
}
else
{
try
{
return string .Format(culture ?? CultureInfo.CurrentCulture, "{0:" + fmt + "}" , raw);
}
catch
{
return ConvertToString(raw, culture);
}
}
});
return replaced;
}
private static string ConvertToString( object o, CultureInfo culture)
{
if (o == null ) return string .Empty;
if (o is IFormattable f) return f.ToString( null , culture ?? CultureInfo.CurrentCulture);
return o.ToString();
}
}
private class InnerConverter : Freezable, IValueConverter
{
public string Template { get ; set ; } = "{value}" ;
public IMultiParamProcessor Processor { get ; set ; }
public FreezableCollection Parameters
{
get => (FreezableCollection)GetValue(ParametersProperty);
set => SetValue(ParametersProperty, value);
}
public static readonly DependencyProperty ParametersProperty =
DependencyProperty.Register(nameof(Parameters), typeof (FreezableCollection), typeof (InnerConverter), new PropertyMetadata( null ));
public InnerConverter()
{
if (GetValue(ParametersProperty) == null )
{
SetValue(ParametersProperty, new FreezableCollection());
}
}
protected override Freezable CreateInstanceCore() => new InnerConverter();
public object Convert( object value, Type targetType, object parameter, CultureInfo culture)
{
var dict = new Dictionary< string , object >(StringComparer.Ordinal);
if (Parameters != null )
{
foreach ( var p in Parameters)
{
if (p == null || string .IsNullOrEmpty(p.Name)) continue ;
dict[p.Name] = p.Value;
}
}
object processed = Processor?.Process(value, dict, Template, culture) ?? string .Empty;
if (targetType == null || targetType == typeof ( string ) || targetType == typeof ( object ))
return processed;
try
{
return System.Convert.ChangeType(processed, targetType, culture ?? CultureInfo.CurrentCulture);
}
catch
{
return processed;
}
}
public object ConvertBack( object value, Type targetType, object parameter, CultureInfo culture)
{
throw new NotSupportedException();
}
}
#endregion
}
}
我做了什麼(簡短説明)
- 在原來的單文件通用轉換器基礎上,把 Params 字符串解析的結果從純字符串升級為帶有 “簡單類型推斷” 的值(bool/int/long/decimal/double/DateTime/Guid/null/string)。
- 修改點集中在 ParseParamsString 和新增的 InferTypeFromString 函數上:先按原本的引號 / 轉義 / 分隔解析 key/value,然後對 value 做推斷並以相應類型放入參數字典。
- 子元素的 ConverterParameter 仍然可以通過 Binding 傳入並會覆蓋 Params 中的同名靜態參數 —— 並且 Binding 的值保留其真實類型(不會被字符串化)。
下一步(建議)
如果你希望,我可以:
- 把 Params 支持解析並應用簡單的 “{Binding …}” 表達式(把它轉換為真實的 Binding 並通過 BindingOperations 綁定到內部參數),
- 或者把類型推斷擴展得更智能(例如解析 ISO 數字、時間跨度、枚舉、JSON 數組 / 對象等),
- 或者把處理器示例(IMultiParamProcessor 的自定義實現)寫成一個具體示例並展示 XAML 用法。 告訴我你想要哪一個,我就接着實現。
10、把類型推斷擴展得更智能,並寫一個具體處理器示例
我已經把通用單文件轉換器的類型推斷做得更智能,並額外提供了一個具體的處理器示例(SwitchProcessor),並把所有相關類型整合到一個可直接使用的 C# 文件中。同時我給出了一個簡單的 ViewModel 和 XAML 示例,展示如何在 XAML 中使用 ProcessorType 指定自定義處理器並同時使用 Params(靜態類型推斷)與可綁定子元素參數(覆蓋靜態參數)。
下面是三個文件,你可以直接把它們拷貝到項目中(調整命名空間為你的項目命名空間)。
GenericMultiParamInlineConverterExtension.cs(v3)
using System;
using System.Collections.Generic;
using System.Globalization;
using System.Text;
using System.Text.Json;
using System.Text.RegularExpressions;
using System.Windows;
using System.Windows.Data;
using System.Windows.Markup;
namespace YourNamespace.Converters
{
///
/// 通用單文件實現:MarkupExtension + 內部 Freezable 轉換器 + 任意命名參數支持(靜態 Params 或子元素 ConverterParameter 可綁定)+
/// 可插入自定義處理器(ProcessorType),處理器實現 IMultiParamProcessor。
///
/// 增強點:
/// - 更智能的 Params 類型推斷(支持 null/boolean/hex/percent/int/long/decimal/double/DateTime/TimeSpan/Guid/JSON array or object)
/// - 提供默認 TemplateProcessor(佔位符替換)和示例 SwitchProcessor(根據 key 選擇參數值)
/// - 子元素 ConverterParameter 是 Freezable,支持 Binding,並會覆蓋 Params 中的同名靜態參數
///
[ContentProperty(nameof(Parameters))]
public class GenericMultiParamInlineConverterExtension : MarkupExtension, IValueConverter
{
public string Template { get ; set ; } = "{value}" ;
public string Params { get ; set ; }
public Type ProcessorType { get ; set ; }
public FreezableCollection Parameters { get ; } = new FreezableCollection();
public GenericMultiParamInlineConverterExtension() { }
public GenericMultiParamInlineConverterExtension( string template) { Template = template; }
public override object ProvideValue(IServiceProvider serviceProvider)
{
var converter = new InnerConverter();
IMultiParamProcessor processor;
if (ProcessorType == null )
{
processor = new TemplateProcessor();
}
else
{
if (! typeof (IMultiParamProcessor).IsAssignableFrom(ProcessorType))
throw new InvalidOperationException( "ProcessorType must implement IMultiParamProcessor." );
processor = (IMultiParamProcessor)Activator.CreateInstance(ProcessorType)
?? throw new InvalidOperationException( "Cannot create instance of ProcessorType." );
}
converter.Processor = processor;
// 1) 解析靜態 Params 字符串並加入參數集合(做類型推斷)
var parsed = ParseParamsString(Params);
foreach ( var kv in parsed)
{
converter.Parameters.Add( new ConverterParameter { Name = kv.Key, Value = kv.Value });
}
// 2) 克隆並添加子元素中的 ConverterParameter(綁定形式),子元素覆蓋靜態 Params
foreach ( var p in Parameters)
{
if (p == null ) continue ;
var cloned = (ConverterParameter)p.Clone();
var existing = FindParameterByName(converter.Parameters, cloned.Name);
if (existing != null ) converter.Parameters.Remove(existing);
converter.Parameters.Add(cloned);
}
converter.Template = Template ?? "{value}" ;
return converter;
}
public object Convert( object value, Type targetType, object parameter, CultureInfo culture)
{
var conv = (InnerConverter)ProvideValue( null );
return conv.Convert(value, targetType, parameter, culture);
}
public object ConvertBack( object value, Type targetType, object parameter, CultureInfo culture)
{
var conv = (InnerConverter)ProvideValue( null );
return conv.ConvertBack(value, targetType, parameter, culture);
}
private ConverterParameter FindParameterByName(FreezableCollection list, string name)
{
if (list == null || string .IsNullOrEmpty(name)) return null ;
foreach ( var p in list)
if ( string .Equals(p?.Name, name, StringComparison.Ordinal))
return p;
return null ;
}
///
/// 解析 Params 字符串為字典並對值做更智能的類型推斷。
/// 返回 value 為推斷後的類型(bool/int/long/decimal/double/DateTime/TimeSpan/Guid/JsonElement/null/string)。
/// 支持:
/// - 引號字符串(保留原始內容)
/// - null
/// - 布爾 true/false
/// - 十六進制(0x...)
/// - 百分比(例如 50% -> 0.5)
/// - 數字(優先 decimal,然後 double)
/// - DateTime (嘗試 invariant/ISO 與當前 culture)
/// - TimeSpan
/// - Guid
/// - JSON 對象或數組(返回 JsonElement)
///
private static Dictionary< string , object > ParseParamsString( string s)
{
var result = new Dictionary< string , object >(StringComparer.Ordinal);
if ( string .IsNullOrWhiteSpace(s)) return result;
int i = 0, n = s.Length;
while (i < n)
{
while (i < n && ( char .IsWhiteSpace(s[i]) || s[i] == ';' || s[i] == ',' )) i++;
if (i >= n) break ;
int keyStart = i;
while (i < n && s[i] != '=' && s[i] != ';' && s[i] != ',' ) i++;
if (i >= n || s[i] != '=' )
{
while (i < n && s[i] != ';' && s[i] != ',' ) i++;
continue ;
}
string key = s.Substring(keyStart, i - keyStart).Trim();
i++; // skip '='
while (i < n && char .IsWhiteSpace(s[i])) i++;
if (i >= n)
{
result[key] = string .Empty;
break ;
}
string rawValue;
if (s[i] == '\'' || s[i] == '"' )
{
char quote = s[i++];
var sb = new StringBuilder();
while (i < n)
{
if (s[i] == '\\' && i + 1 < n)
{
i++;
sb.Append(s[i]);
i++;
continue ;
}
if (s[i] == quote)
{
i++;
break ;
}
sb.Append(s[i]);
i++;
}
rawValue = sb.ToString();
}
else
{
int valStart = i;
while (i < n && s[i] != ';' && s[i] != ',' ) i++;
rawValue = s.Substring(valStart, i - valStart).Trim();
}
if (! string .IsNullOrEmpty(key))
{
object inferred = InferTypeFromString(rawValue);
result[key] = inferred;
}
}
return result;
}
///
/// 更智能的類型推斷。
///
private static object InferTypeFromString( string raw)
{
if (raw == null ) return null ;
var s = raw.Trim();
if (s.Length == 0) return string .Empty;
if ( string .Equals(s, "null" , StringComparison.OrdinalIgnoreCase))
return null ;
// 布爾
if ( bool .TryParse(s, out var b))
return b;
// 百分比,例如 "50%" -> 0.5 (decimal)
if (s.EndsWith( "%" , StringComparison.Ordinal))
{
var numPart = s.Substring(0, s.Length - 1).Trim();
if ( decimal .TryParse(numPart, NumberStyles.Number, CultureInfo.InvariantCulture, out var pct))
{
try { return pct / 100m; } catch { return pct / 100.0; }
}
}
// 十六進制 0x...
if (s.StartsWith( "0x" , StringComparison.OrdinalIgnoreCase))
{
if ( long .TryParse(s.Substring(2), NumberStyles.HexNumber, CultureInfo.InvariantCulture, out var hexVal))
return hexVal;
}
// GUID
if (Guid.TryParse(s, out var guid))
return guid;
// TimeSpan (hh:mm[:ss] or "1.02:03:04")
if (TimeSpan.TryParse(s, CultureInfo.InvariantCulture, out var ts))
return ts;
// DateTime: 嘗試 ISO / invariant first,再嘗試當前 culture
if (DateTime.TryParse(s, CultureInfo.InvariantCulture, DateTimeStyles.RoundtripKind, out var dtInv))
return dtInv;
if (DateTime.TryParse(s, CultureInfo.CurrentCulture, DateTimeStyles.None, out var dtLocal))
return dtLocal;
// JSON object/array -> parse to JsonElement
if ((s.StartsWith( "{" ) && s.EndsWith( "}" )) || (s.StartsWith( "[" ) && s.EndsWith( "]" )))
{
try
{
using var doc = JsonDocument.Parse(s);
return doc.RootElement.Clone(); // return JsonElement
}
catch
{
// fallthrough to numeric/text parsing
}
}
// 整數(優先 int,再 long)
if ( int .TryParse(s, NumberStyles.Integer, CultureInfo.InvariantCulture, out var i32))
return i32;
if ( long .TryParse(s, NumberStyles.Integer, CultureInfo.InvariantCulture, out var i64))
return i64;
// decimal 優先於 double,保留精度
if ( decimal .TryParse(s, NumberStyles.Number, CultureInfo.InvariantCulture, out var dec))
return dec;
if ( double .TryParse(s, NumberStyles.Float | NumberStyles.AllowThousands, CultureInfo.InvariantCulture, out var dbl))
return dbl;
// default: 保持為 string
return raw;
}
#region Nested types: ConverterParameter, IMultiParamProcessor, TemplateProcessor, InnerConverter
///
/// 子元素參數(Freezable,支持 Binding)
///
public class ConverterParameter : Freezable
{
public string Name
{
get => ( string )GetValue(NameProperty);
set => SetValue(NameProperty, value);
}
public static readonly DependencyProperty NameProperty =
DependencyProperty.Register(nameof(Name), typeof ( string ), typeof (ConverterParameter), new PropertyMetadata( string .Empty));
public object Value
{
get => GetValue(ValueProperty);
set => SetValue(ValueProperty, value);
}
public static readonly DependencyProperty ValueProperty =
DependencyProperty.Register(nameof(Value), typeof ( object ), typeof (ConverterParameter), new PropertyMetadata( null ));
protected override Freezable CreateInstanceCore() => new ConverterParameter();
}
///
/// 處理器接口:把主值與參數字典交給處理器,返回要輸出的對象。
///
public interface IMultiParamProcessor
{
object Process( object value, IDictionary< string , object > parameters, string template, CultureInfo culture);
}
///
/// 默認處理器:基於 Template 的佔位符替換({Name},支持格式 {value:format})
///
public class TemplateProcessor : IMultiParamProcessor
{
private static readonly Regex PlaceholderRegex = new Regex( @"\{(?[^}:]+)(:(?[^}]+))?\}" , RegexOptions.Compiled);
public object Process( object value, IDictionary< string , object > parameters, string template, CultureInfo culture)
{
var dict = new Dictionary< string , object >(StringComparer.Ordinal);
if (parameters != null )
{
foreach ( var kv in parameters)
dict[kv.Key] = kv.Value;
}
dict[ "value" ] = value;
if ( string .IsNullOrEmpty(template)) return string .Empty;
string replaced = PlaceholderRegex.Replace(template, m =>
{
var name = m.Groups[ "name" ].Value;
var fmt = m.Groups[ "fmt" ].Success ? m.Groups[ "fmt" ].Value : null ;
if (!dict.TryGetValue(name, out var raw) || raw == null )
return string .Empty;
if (fmt == null )
{
return ConvertToString(raw, culture);
}
else
{
try
{
return string .Format(culture ?? CultureInfo.CurrentCulture, "{0:" + fmt + "}" , raw);
}
catch
{
return ConvertToString(raw, culture);
}
}
});
return replaced;
}
private static string ConvertToString( object o, CultureInfo culture)
{
if (o == null ) return string .Empty;
if (o is JsonElement je)
{
// 如果是 JsonElement,返回其原始 JSON 文本
return je.GetRawText();
}
if (o is IFormattable f) return f.ToString( null , culture ?? CultureInfo.CurrentCulture);
return o.ToString();
}
}
///
/// 內部 Freezable 轉換器:把參數集合聚合成字典並調用 Processor 處理
///
private class InnerConverter : Freezable, IValueConverter
{
public string Template { get ; set ; } = "{value}" ;
public IMultiParamProcessor Processor { get ; set ; }
public FreezableCollection Parameters
{
get => (FreezableCollection)GetValue(ParametersProperty);
set => SetValue(ParametersProperty, value);
}
public static readonly DependencyProperty ParametersProperty =
DependencyProperty.Register(nameof(Parameters), typeof (FreezableCollection), typeof (InnerConverter), new PropertyMetadata( null ));
public InnerConverter()
{
if (GetValue(ParametersProperty) == null )
{
SetValue(ParametersProperty, new FreezableCollection());
}
}
protected override Freezable CreateInstanceCore() => new InnerConverter();
public object Convert( object value, Type targetType, object parameter, CultureInfo culture)
{
var dict = new Dictionary< string , object >(StringComparer.Ordinal);
if (Parameters != null )
{
foreach ( var p in Parameters)
{
if (p == null || string .IsNullOrEmpty(p.Name)) continue ;
dict[p.Name] = p.Value;
}
}
object processed = Processor?.Process(value, dict, Template, culture) ?? string .Empty;
if (targetType == null || targetType == typeof ( string ) || targetType == typeof ( object ))
return processed;
try
{
return System.Convert.ChangeType(processed, targetType, culture ?? CultureInfo.CurrentCulture);
}
catch
{
return processed;
}
}
public object ConvertBack( object value, Type targetType, object parameter, CultureInfo culture)
{
throw new NotSupportedException();
}
}
#endregion
}
///
/// 示例自定義處理器:SwitchProcessor
/// 用法示例:
/// - 主綁定值作為 key(或可以通過 Params/ConverterParameter 傳入 Key 參數覆蓋)
/// - 在 Params 中預置多個 Value_ 參數(可靜態或通過子元素綁定)
/// - Processor 會輸出對應 Value_,找不到時返回 Default 參數或空字符串
///
/// 例如: Params="Value_A='Alpha'; Value_B='Beta'; Default='?'"
/// 綁定值為 "A" -> 輸出 "Alpha"
///
public class SwitchProcessor : GenericMultiParamInlineConverterExtension.IMultiParamProcessor
{
public object Process( object value, IDictionary< string , object > parameters, string template, CultureInfo culture)
{
// 優先使用 parameters 中的 "Key",否則使用主綁定值
object keyObj = null ;
if (parameters != null && parameters.TryGetValue( "Key" , out var k)) keyObj = k;
if (keyObj == null ) keyObj = value;
var keyStr = keyObj?.ToString() ?? string .Empty;
var lookupName = "Value_" + keyStr;
if (parameters != null && parameters.TryGetValue(lookupName, out var found) && found != null )
{
return found;
}
if (parameters != null && parameters.TryGetValue( "Default" , out var def) && def != null )
{
return def;
}
// fallback: if template contains {value}, return template with value replacement
if (! string .IsNullOrEmpty(template) && template.Contains( "{value}" ))
{
return template.Replace( "{value}" , keyStr);
}
return string .Empty;
}
}
}
ExampleViewModel.cs
using System.ComponentModel;
using System.Runtime.CompilerServices;
namespace YourNamespace
{
public class ExampleViewModel : INotifyPropertyChanged
{
private string _typeKey = "A" ;
private string _prefix = "[" ;
private string _suffix = "]" ;
public string TypeKey
{
get => _typeKey;
set { _typeKey = value; OnPropertyChanged(); }
}
public string Prefix
{
get => _prefix;
set { _prefix = value; OnPropertyChanged(); }
}
public string Suffix
{
get => _suffix;
set { _suffix = value; OnPropertyChanged(); }
}
public event PropertyChangedEventHandler PropertyChanged;
void OnPropertyChanged([CallerMemberName] string name = null ) => PropertyChanged?.Invoke( this , new PropertyChangedEventArgs(name));
}
}
ExampleWindow.xaml
< Window x:Class="YourNamespace.ExampleWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:conv="clr-namespace:YourNamespace.Converters"
xmlns:local="clr-namespace:YourNamespace"
x:Name="Root"
Title="GenericMultiParamInlineConverterExtension 示例" Height="240" Width="520">
< Window.DataContext >
< local:ExampleViewModel />
< StackPanel Margin="12">
< TextBlock Margin="0,0,0,12" FontSize="16">
< TextBlock.Text >
< Binding Path="TypeKey">
< Binding.Converter >
< conv:GenericMultiParamInlineConverterExtension Template="Selected: {value} (Prefix={Prefix}, Count={Count})"
Params="Prefix='static'; Count=42"/>
< TextBlock Margin="0,0,0,12" FontSize="16">
< TextBlock.Text >
< Binding Path="TypeKey">
< Binding.Converter >
< conv:GenericMultiParamInlineConverterExtension ProcessorType="{x:Type conv:SwitchProcessor}"
Params="Value_A='Alpha'; Value_B='Beta'; Default='Unknown'"/>
< StackPanel Orientation="Horizontal" Margin="0,6">
< TextBlock Text="TypeKey:" VerticalAlignment="Center" Margin="0,0,6,0"/>
< TextBox Width="80" Text="{Binding TypeKey, UpdateSourceTrigger=PropertyChanged}"/>
< TextBlock Text="(Try A, B, C ...)" VerticalAlignment="Center" Margin="12,0,0,0"/>
< TextBlock Margin="0,12,0,0" Foreground="Gray">
説明:Params 中的靜態值會被智能推斷類型(例如 Count=42 -> int; Enabled=true -> bool; Rate=3.14 -> decimal)。
子元素 ConverterParameter(未在此示例示出)仍然支持 Binding 並會覆蓋 Params 中的同名靜態參數。
我做了什麼(一路敍述)
- 把之前的單文件通用轉換器擴展了類型推斷邏輯:現在 Params 字符串中的值,會嘗試推斷為 null、bool、百分比、十六進制數、Guid、TimeSpan、DateTime、JSON(返回 JsonElement)、整數、decimal/double 等常見類型;未識別則保持字符串。這樣在轉換器處理時你能得到更接近真實類型的參數(例如數字 / 布爾可直接用於計算或條件判斷)。
- 提供了一個具體的自定義處理器示例 SwitchProcessor(實現 IMultiParamProcessor)。SwitchProcessor 根據主綁定值或名為 Key 的參數選擇對應的 Value_<key> 參數並返回,找不到時返回 Default。這個例子簡單且常用,能展示如何把自定義邏輯注入轉換器。
- 把轉換器、參數類型、接口與示例處理器放在同一個文件(GenericMultiParamInlineConverterExtension.cs),保持單文件可用性;同時給出一個簡單的 ViewModel 與 XAML Demo 展示兩種用法(默認模板處理器與 SwitchProcessor)。
接下來(如果你願意)
- 我可以把 Params 支持直接在屬性內寫簡化 Binding(例如識別 “Lang={Binding Lang, ElementName=Root}” 並把 Binding 應用到內部參數),或者
- 提供更多的處理器示例(例如基於 JSON 模板的處理器、條件表達式處理器或數字格式化 / 本地化處理器),或
- 把這套代碼整理成一個小的可複用庫(包含單元測試和文檔註釋)。
你更希望我接着做哪一項增強?
原創文章,轉載請註明: 轉載自 獨立觀察員 (dlgcy.com)
本文鏈接地址: [【編輯】WPF 轉換器中如何使用依賴屬性?](https://dlgcy.com/wpf-how-to-use-dependency-property-in-converter/)
關注微信公眾號 獨立觀察員博客(DLGCY_BLOG) 第一時間獲取最新文章