Привязка к текущему DataContext с помощью Converter с использованием x:Bind

У меня есть следующий преобразователь:

public class MyConverter : IValueConverter
{
    public object Convert(object value, Type targetType, object parameter, string language)
    {
         Debug.WriteLine(value.GetType());             

         //The rest of the code             
    }

    public object ConvertBack(object value, Type targetType, object parameter, string language)
    {
        throw new NotImplementedException();
    }
}

И XAML, который пытается использовать преобразователь:

<ListView ItemsSource="{x:Bind StickersCVS.View}" >
    <ListView.ItemTemplate>
        <DataTemplate x:DataType="models:StickerCategory">
            <TextBlock Foreground="{x:Bind Converter={StaticResource MyConverter}}"/>
        </DataTemplate>
    </ListView.ItemTemplate>
</ListView>

Это дает мне NPE в value.GetType(), по-видимому, переданное значение равно null.

Если я изменю следующую часть:

<TextBlock Foreground="{x:Bind Converter={StaticResource MyConverter}}"/>

to

<TextBlock Foreground="{Binding Converter={StaticResource MyConverter}}"/>

Тогда это работает. Debug правильно выводит StickerCategory как тип значения. Любая причина, почему x:Bind передает null в преобразователь и как заставить его работать с x:Bind? Я пытаюсь передать DataContext своему преобразователю.


person Andreas Wong    schedule 13.11.2015    source источник
comment
Обратите внимание, что режим по умолчанию для x:Bind — OneTime (привязка по умолчанию — OneWay). Может ли это быть первопричиной? Возможно, значение действительно равно нулю при первой привязке....   -  person gregkalapos    schedule 13.11.2015
comment
Неа, без кубиков :(, попробовал указать режим привязки: <TextBlock Text="{x:Bind Action}" Foreground="{x:Bind Converter={StaticResource MyConverter, Mode=OneWay}"/> Всё равно NPE. Всё равно спасибо :)   -  person Andreas Wong    schedule 13.11.2015
comment
ты действительно в этом уверен? Очень вероятно, что с Mode=OneWay или без него он сначала будет нулевым, но без него он никогда не изменится. Таким образом, поскольку в вашем конвертере нет проверки на нулевое значение, ваша программа, вероятно, никогда не дойдет до случая, когда на самом деле присутствует значение?!   -  person Markus Hütter    schedule 14.11.2015


Ответы (3)


{x:Bind} использует сгенерированный код для достижения своих преимуществ, и при использовании разных Path в {x:Bind} сгенерированный код имеет некоторые отличия.

Здесь я использую простой пример, например. Полный пример можно найти на GitHub.

В примере у меня есть ViewModel, как показано ниже:

public class MyViewModel
{
    public MyViewModel()
    {
        MyList = new List<Item>()
        {
            new Item {Name="1",Number=1 },
            new Item {Name="2",Number=2 },
            new Item {Name="3",Number=3 }
        };
    }

    public List<Item> MyList { get; set; }
}

public class Item
{
    public string Name { get; set; }
    public int Number { get; set; }

    public override string ToString()
    {
        return string.Format("Name: {0}, Number {1}", this.Name, this.Number);
    }
}

Когда мы используем {x:Bind Name, Converter={StaticResource ItemConvert}} в MainPage.xaml

<ListView ItemsSource="{x:Bind ViewModel.MyList}">
    <ListView.ItemTemplate>
        <DataTemplate x:DataType="local:Item">
            <TextBlock Text="{x:Bind Converter={StaticResource ItemConvert}}" />
        </DataTemplate>
    </ListView.ItemTemplate>
</ListView>

Он генерирует следующий код в MainPage.g.cs

public void DataContextChangedHandler(global::Windows.UI.Xaml.FrameworkElement sender, global::Windows.UI.Xaml.DataContextChangedEventArgs args)
{
     global::xBindWithConverter.Item data = args.NewValue as global::xBindWithConverter.Item;
     if (args.NewValue != null && data == null)
     {
        throw new global::System.ArgumentException("Incorrect type passed into template. Based on the x:DataType global::xBindWithConverter.Item was expected.");
     }
     this.SetDataRoot(data);
     this.Update();
}

// IDataTemplateExtension

public bool ProcessBinding(uint phase)
{
    throw new global::System.NotImplementedException();
}

public int ProcessBindings(global::Windows.UI.Xaml.Controls.ContainerContentChangingEventArgs args)
{
    int nextPhase = -1;
    switch(args.Phase)
    {
        case 0:
            nextPhase = -1;
            this.SetDataRoot(args.Item as global::xBindWithConverter.Item);
            if (!removedDataContextHandler)
            {
                removedDataContextHandler = true;
                ((global::Windows.UI.Xaml.Controls.TextBlock)args.ItemContainer.ContentTemplateRoot).DataContextChanged -= this.DataContextChangedHandler;
            }
            this.initialized = true;
            break;
    }
    this.Update_((global::xBindWithConverter.Item) args.Item, 1 << (int)args.Phase);
    return nextPhase;
}
...
public void Update()
{
    this.Update_(this.dataRoot, NOT_PHASED);
    this.initialized = true;
}

И

global::Windows.UI.Xaml.Controls.TextBlock element3 = (global::Windows.UI.Xaml.Controls.TextBlock)target;
MainPage_obj3_Bindings bindings = new MainPage_obj3_Bindings();
returnValue = bindings;
bindings.SetDataRoot((global::xBindWithConverter.Item) element3.DataContext);
bindings.SetConverterLookupRoot(this);
element3.DataContextChanged += bindings.DataContextChangedHandler;
global::Windows.UI.Xaml.DataTemplate.SetExtensionInstance(element3, bindings);

При инициализации страницы element3.DataContextChanged += bindings.DataContextChangedHandler; будет выполняться в первую очередь. После этого будет вызываться метод DataContextChangedHandler, так как при инициализации возникает событие DataContextChanged. И метод ProcessBindings будет выполняться для обновления элемента контейнера элемента списка со связанными данными.

В методе DataContextChangedHandler он вызывает метод this.Update();, который в конце вызывает метод Update_(global::xBindWithConverter.Item obj, int phase). Но когда вызывается метод DataContextChangedHandler, его args.NewValue значение равно null, поэтому obj в методе Update_(global::xBindWithConverter.Item obj, int phase) также равно null.

И при использовании {x:Bind Converter={StaticResource ItemConvert}} в XAML сгенерированный код для Update_(global::xBindWithConverter.Item obj, int phase):

// Update methods for each path node used in binding steps.
private void Update_(global::xBindWithConverter.Item obj, int phase)
{
    if((phase & ((1 << 0) | NOT_PHASED )) != 0)
    {
        XamlBindingSetters.Set_Windows_UI_Xaml_Controls_TextBlock_Text(this.obj3.Target as global::Windows.UI.Xaml.Controls.TextBlock, (global::System.String)this.LookupConverter("ItemConvert").Convert(obj, typeof(global::System.String), null, null), null);
    }
}

Поскольку obj — это null, то value в вашем Convert — это null, и, наконец, он бросает NPE в value.GetType().

Но если мы используем другой Path в {x:Bind}, например {x:Bind Name, Converter={StaticResource ItemConvert}}, сгенерированный код для Update_(global::xBindWithConverter.Item obj, int phase) будет другим:

// Update methods for each path node used in binding steps.
private void Update_(global::xBindWithConverter.Item obj, int phase)
{
    if (obj != null)
    {
        if ((phase & (NOT_PHASED | (1 << 0))) != 0)
        {
            this.Update_Name(obj.Name, phase);
        }
    }
}
private void Update_Name(global::System.String obj, int phase)
{
    if((phase & ((1 << 0) | NOT_PHASED )) != 0)
    {
        XamlBindingSetters.Set_Windows_UI_Xaml_Controls_TextBlock_Text(this.obj3.Target as global::Windows.UI.Xaml.Controls.TextBlock, (global::System.String)this.LookupConverter("ItemConvert").Convert(obj, typeof(global::System.String), null, null), null);
    }
}

Он определит, является ли obj null. Таким образом, метод XamlBindingSetters.Set_Windows_UI_Xaml_Controls_TextBlock_Text здесь вызываться не будет, а NullReferenceException не произойдет.

Чтобы решить эту проблему, как сказал @Markus Hütter, вы можете добавить проверку null в свой конвертер, например:

public object Convert(object value, Type targetType, object parameter, string language)
{
    if (value != null)
    {
        System.Diagnostics.Debug.WriteLine(value.GetType());
        return value.ToString();
    }
    else
    {
        System.Diagnostics.Debug.WriteLine("value is null");
        return null;
    }
}
person Jay Zuo    schedule 13.11.2015

Я не думаю, что это хорошая идея использовать x:bind с конвертером в целом, цель использования x:bing - повысить производительность, а конвертер резко повлияет на производительность кода. вы можете легко создать поле только для чтения в своей модели, и когда ваши данные будут изменены, вы можете вызвать событие изменения свойства и преобразовать число туда.

Например,

[JsonIgnore]
    public double IsUnreadOpacity
    {
        get { return (IsUnread) ? 1 : 0; }
    }

public bool IsUnread
    {
        get { return _isUnread; }
        set
        {
            if (value == _isUnread)
                return;
            Set(ref _isUnread, value);
            RaisePropertyChanged(nameof(IsUnreadOpacity));
        }
    }
person apramc    schedule 28.03.2016

Однако, начиная с версии 1607, вы можете использовать функции с x:Bind, и это открывает новые возможности: быстрое преобразование без дополнительной сложности конвертеров. См. документацию.

person Gábor    schedule 02.12.2020