简体   繁体   中英

Binding to current DataContext with Converter using x:Bind

I have the following converter:

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();
    }
}

And the XAML that attempts to use the converter:

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

This gives me an NPE at value.GetType() , apparently the value passed in is null .

If I change the following part:

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

to

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

Then it works. The Debug correctly outputs StickerCategory as the type of the value. Any reason why x:Bind passes null into the converter and how do I make it work with x:Bind ? I'm trying to pass DataContext to my converter.

{x:Bind} uses generated code to achieve its benefits and while using different Path in {x:Bind} , the generated code has some differences.

Here I use a simple sample for example. For the complete sample, please check at GitHub .

In the sample, I have a ViewModel like following:

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);
    }
}

When we use {x:Bind Name, Converter={StaticResource ItemConvert}} in 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>

It generates following code in 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;
}

And

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);

When initializing the Page, element3.DataContextChanged += bindings.DataContextChangedHandler; will be executed firstly. After this, DataContextChangedHandler method will be called as DataContextChanged event is raised while initializing. And the ProcessBindings method will be executed to update list item container element with bound data.

In the DataContextChangedHandler method, it calls this.Update(); method which calls Update_(global::xBindWithConverter.Item obj, int phase) method in the end. But when the DataContextChangedHandler method is called, it args.NewValue value is null , so the obj in Update_(global::xBindWithConverter.Item obj, int phase) method is also null .

And when using {x:Bind Converter={StaticResource ItemConvert}} in XAML, the generated code for Update_(global::xBindWithConverter.Item obj, int phase) is:

// 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);
    }
}

As the obj is null , so the value in your Convert is null and finally it throws a NPE at value.GetType() .

But if we use another Path in {x:Bind} like {x:Bind Name, Converter={StaticResource ItemConvert}} , the generated code for Update_(global::xBindWithConverter.Item obj, int phase) is different:

// 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);
    }
}

It will determine whether the obj is null . So the XamlBindingSetters.Set_Windows_UI_Xaml_Controls_TextBlock_Text method won't be called here and the NullReferenceException won't occur.

To solve the problem, just like @Markus Hütter said, you can add a null check in your converter like:

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;
    }
}

I don't think it is a good idea to use x:bind with converter in general, the goal of using x:bing is to increase performance, while converter will dramatically affect code's performance. you can easily create a readonly field in your model, and when your data is changed, you can raise property changed event, and convert the number there.

For example,

[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));
        }
    }

However, since version 1607 you can use functions with x:Bind and this opens up new opportunities: quick conversion without the added complexity of converters. See the documentation.

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM