簡體   English   中英

WPF 綁定僅在使用轉換器時有效

[英]WPF Binding only working when using a Converter

我正在努力使用可重用且相對簡單的 UserControl。

我的數據存儲在實現簡單 IHasValue 接口的類中:

public interface IHasValue<T>
{
    T Value { get; }
    ValueType Type { get; }
}

我需要一個可重用的控件來編輯應用程序周圍多個位置的數據。 這是我目前擁有的控件,當 SelectedValue 為空時,它只顯示一個帶加號的按鈕,單擊它會設置 SelectedValue(稍后我將添加用戶輸入)並顯示值而不是按鈕:

<UserControl x:Class="DotsCompanion.Controls.HasValueFloatSelectorControl"
             xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
             xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
             xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" 
             xmlns:d="http://schemas.microsoft.com/expression/blend/2008" 
             xmlns:local="clr-namespace:DotsCompanion.Controls"
             xmlns:conv="clr-namespace:DotsCompanion.Converters"
             mc:Ignorable="d" 
             d:DesignHeight="450" d:DesignWidth="800">


    <UserControl.Resources>
        <conv:NullToTrueFalseConverter x:Key="NullToTrueFalseConverter" />
        <conv:MyDebugConverter x:Key="MyDebugConverter" />

        <DataTemplate x:Key="EmptyValueTemplate" DataType="{x:Type ContentControl}">
            <Button Click="NewValueClick">
                <TextBlock Text="+"></TextBlock>
            </Button>
        </DataTemplate>
        <DataTemplate x:Key="HasValueTemplate" DataType="{x:Type ContentControl}">
            <StackPanel DataContext="{Binding Path=DataContext, RelativeSource={RelativeSource AncestorType={x:Type ContentControl}}}">
                <!-- THIS IS THE PROBLEMATIC BINDING -->
                <TextBlock Text="{Binding Path=SelectedValue, Converter={StaticResource MyDebugConverter}}"></TextBlock>
            </StackPanel>
        </DataTemplate>
    </UserControl.Resources>


    <Grid Name="LayoutRoot">
        <ContentControl>
            <ContentControl.Style>
                <Style TargetType="ContentControl">
                    <Style.Triggers>
                        <DataTrigger Binding="{Binding SelectedValue, Converter={StaticResource NullToTrueFalseConverter}}" Value="true">
                            <Setter Property="ContentTemplate" Value="{DynamicResource HasValueTemplate}"/>
                        </DataTrigger>
                        <DataTrigger Binding="{Binding SelectedValue, Converter={StaticResource NullToTrueFalseConverter}}" Value="false">
                            <Setter Property="ContentTemplate" Value="{DynamicResource EmptyValueTemplate}"/>
                        </DataTrigger>
                    </Style.Triggers>
                </Style>
            </ContentControl.Style>
        </ContentControl>
    </Grid>
</UserControl>

這是控件的極其簡單的代碼隱藏:

public partial class HasValueFloatSelectorControl : UserControl
{
    public IHasValue<float> SelectedValue
    {
        get { return (IHasValue<float>)GetValue(SelectedValueProperty); }
        set { SetValue(SelectedValueProperty, value); }
    }

    public static readonly DependencyProperty SelectedValueProperty =
        DependencyProperty.Register("SelectedValue", typeof(IHasValue<float>), typeof(HasValueFloatSelectorControl), new PropertyMetadata(default(IHasValue<float>)));


    public HasValueFloatSelectorControl()
    {
        InitializeComponent();
        LayoutRoot.DataContext = this;
    }

    private void NewValueClick(object sender, EventArgs e)
    {
        SelectedValue = new FloatPrimitive(2.0f);
    }
}

FloatPrimitive 是實際存儲數據(浮點數)的類,它實現 IHasValue 並覆蓋 ToString() 以便將浮點數顯示為字符串。

問題是,將 Text 綁定到 SelectedValue 似乎僅在使用轉換器時才有效。 在上面的代碼中,MyDebugConverter 只是按原樣返回值:

// This should be literally useless AFAIK
public class MyDebugConverter : IValueConverter
{
    public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
    {
        return value;
    }

    public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
    {
        return value;
    }
}

如果我移除轉換器,綁定只會顯示任何內容。 根據輸出窗口跟蹤,DataItem 為空,但使用實時屬性瀏覽器我可以看到正確設置了 DataContext,而 SelectedValue 確實是一個 FloatPrimitive。

我嘗試尋找解決方案,但到目前為止,我只找到了相反的情況(綁定不能轉換器一起使用)。 我對 WPF 還很陌生,所以我不確定從哪里開始調試,因為這似乎是一種相當罕見的情況,目前在線資源也無濟於事。

我試過的:

  • 通過在 LayoutRoot Grid 內添加一個簡單的 TextBlock 綁定到 DataTemplate之外的 SelectedValue,但我得到了非常相同的結果(需要轉換器);
  • 通過從控件外部綁定到它並通過在控件內部調用 NewValueClick() 方法來設置 SelectedValue;
  • 在 MyDebugConverter 中添加一個 Debugger.Break() 調用,它允許我檢查該值是否為空並正確設置,但是再次使用轉換器是允許它首先正確顯示的原因;
  • 大量擺弄實時財產瀏覽器

這可能是因為我正在使用接口嗎? 我對項目中的另一個用戶控件有一個非常相似的設置,它工作得很好,但它使用抽象類 + 繼承而不是接口 + 實現。

顯然,問題在於您的屬性SelectedValueinterface類型。

如果您將SelectedItem的類型更改為FloatPrimitive ,則一切都按預期工作(不是解決方案,只是對我的觀點的測試)。 我還通過測試和研究確認問題在於interface方面,而不是它是泛型類型的事實。


問題說明

WPF 在底層使用類型轉換器來處理綁定更新期間類型之間的轉換。

  • 對於IntegerString等類型,有一堆默認的TypeConverter
  • 通過將TypeConverterAttribute應用於要轉換的類,您可以為自己的類定義TypeConverter
  • 當 WPF 轉換為String ,如果所有其他方法都失敗了,它將調用Object.ToString()

問題是:正如在這個答案中所解釋的,當 WPF 處理數據綁定時,它不會檢查正在傳輸的值的實際類型,它只查看源和目標屬性的聲明類型。 出於這個原因,您在FloatPrimitive聲明什么轉換運算符或是否覆蓋ToString() FloatPrimitive WPF 只能看到IHasValue<T>因為這是SelectedItem屬性的類型。

IHasValue<T>顯然沒有默認的TypeConverter並且您還沒有聲明自己的。 由於TextBlock.TextString ,WPF 通常會嘗試調用IHasValue<T> Object.ToString() ,但IHasValue<T>不是Object 實現IHasValue<T>Object肯定是Object的后代,但 WPF 不知道這一點,它只知道它是一個IHasValue<T> 所以轉換失敗,結果返回null

您實際上可以看到此轉換失敗。 如果您將PresentationTraceSources.TraceLevel=High添加到您的{Binding SelectedItem ...} (並離開Converter=... ),輸出將顯示:

System.Windows.Data Warning: 101 : BindingExpression (hash=16490914): GetValue at level 0 from HasValueFloatSelectorControl (hash=72766) using DependencyProperty(SelectedValue): FloatPrimitive (hash=14200498)
System.Windows.Data Warning: 80 : BindingExpression (hash=16490914): TransferValue - got raw value FloatPrimitive (hash=14200498)
System.Windows.Data Warning: 84 : BindingExpression (hash=16490914): TransferValue - implicit converter produced <null>
System.Windows.Data Warning: 89 : BindingExpression (hash=16490914): TransferValue - using final value <null>

綁定正確獲取FloatPrimitive ,嘗試轉換它,但最終為null ,這解釋了TextBlock為空的原因。

顯而易見的解決方案是為調用ToString() IHasValue<T>聲明您自己的TypeConverter 不幸的是,這不起作用(我自己嘗試過)。 似乎 WPF 只是不支持interfaceTypeConverter 這個問題顯示了一個有類似問題的人(他們試圖轉換interface類型)。

為什么MyDebugConverter修復了問題

當您使用Binding.Converter ,WPF 將在嘗試其他任何操作之前調用提供的IValueConverter 但是IValueConverter不需要返回目標屬性的確切類型,因此 WPF 需要檢查輸出值的類型。 如果IValueConverter輸出不是目標屬性的確切類型,WPF 將嘗試使用TypeConverter來完成工作。

問題是,這需要 WPF 實際檢查輸出值的類型。 因此,即使MyDebugConverter什么都不做,WPF 也假定輸入是IHasValue<T> ,但實際上查看了輸出並發現它現在是FloatPrimitive 所以現在它嘗試將其轉換為后者而不是前者。 這允許它使用適用的默認TypeConverter來調用Object.ToString()


解決方案

如上所述,聲明您自己的TypeConverter不適用於interface類型,這讓您有幾個選擇:

  • SelectedItem的類型更改為某個基class ,而不是interface (根據您的要求,這可能不是一種選擇)
  • Text (或類似名稱)屬性添加到您的IHasValue<T>實現(可能還有interface本身),它將返回ToString() 綁定到該屬性而不是對象。
    例如,如果FloatPrimitive有一個public string Text => ToString(); ,然后您可以綁定到SelectedItem.Text
  • 不要綁定String屬性。 如果你用ContentControl替換你的TextBlock並綁定ContentControl.Content ,一切都會按預期顯示(內部ContentPresenter知道調用ToString() )。 這也為您提供了添加DataTemplate的選項,以防您想顯示的不僅僅是文本。 示例: <ContentControl Content="{Binding SelectedValue}"/>
  • 綁定到String類型或其他類型屬性時,將Binding.ConverterIValueConverter一起使用。

暫無
暫無

聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.

 
粵ICP備18138465號  © 2020-2024 STACKOOM.COM