Vincenzo Petronio
Vincenzo Petronio

Reputation: 191

ValueConverter called multiple times during navigation

There's something i can't understand in the Converter used with Binding. :-(

I created a simple example with Mvvm-Light ...

here the full solution: https://testbindingwithconverter.codeplex.com/SourceControl/latest

here an image: https://i.sstatic.net/wUf89.png

and below the summary source code:

CarsView

<Rectangle Grid.Row="0" 
               Opacity="{Binding SelectedCar, Converter={StaticResource IntToOpacityConverter}}" 
               Fill="#FFD1E22A"  />
    <ListBox Grid.Row="1" 
             ItemsSource="{Binding Cars}" 
             SelectedItem="{Binding SelectedCar, Mode=TwoWay}" >
        <ListBox.ItemTemplate>
            <DataTemplate>
                    <StackPanel>
                        <TextBlock Text="{Binding Company}" />
                        <TextBlock Text="{Binding Name}" />
                        <TextBlock Text="{Binding Year}" />
                    </StackPanel>
            </DataTemplate>
        </ListBox.ItemTemplate>
    </ListBox>

CarsViewModel

public class CarsViewModel : ViewModelBase
{
    private ObservableCollection<Car> cars;
    private Car selectedCar;

    public CarsViewModel()
    {
        if (IsInDesignMode)
        {
        }
        else
        {
            // INIT
            this.cars = new ObservableCollection<Car>();
            this.selectedCar = null;

            // FAKE DATA
            for (int i = 1; i <= 10; i++)
            {
                Car newCar = new Car { Id = i, Company = "Company_" + i, Name = "Name_" + i, Year = "200" + i };
                Cars.Add(newCar);
            }
        }
    }

    public ObservableCollection<Car> Cars
    {
        get 
        {
            return cars; 
        }

        set 
        {
            if (cars != value)
            {
                cars = value;
                RaisePropertyChanged(() => Cars);
            }
        }
    }

    public Car SelectedCar
    {
        get
        {
            return selectedCar;
        }

        set
        {
            if (value != selectedCar)
            {
                selectedCar = value;
                RaisePropertyChanged(() => SelectedCar);
            }
        }
    }
}

Converter

public class IntToOpacityConverter : System.Windows.Data.IValueConverter
{
    public object Convert(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture)
    {
        double opacity = 1;
        Car c = value as Car;

        if (c != null)
        {
            if (c.Id == 5)
            {
                opacity = 0.3;
            }
        }

        System.Diagnostics.Debug.WriteLine("[IntToOpacityConverter] \t" + "Rectangle Opacity: " + opacity);
        return opacity;
    }

    public object ConvertBack(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture)
    {
        // One-Way conversion!
        return null;
    }

From the CarsView, when i tap on item 5 of the ListBox, the property Opacity of Rectangle is set to the value 0.3, otherwise to the value 1.

Now, when i go back to the HomeView and then again to the CarsView, if i tap between items the converter is called 2 times! And again, if i go back to the HomeView and return to the CarsView, the converter is called 3 times! And so on...

Why?

Upvotes: 2

Views: 1024

Answers (3)

not exactly answering this question, but for anyone who may be helped with this:

wpf convert and convertBack funcs can conflict and overload your app when used with DataGrid auto generating functionality even if you turn that feature off, so listView (or list view with gridView mode) should be used for when you want to use Convert/ConvertBack

Upvotes: 0

Chubosaurus Software
Chubosaurus Software

Reputation: 8161

Hey Vincenzo Petronio,

I would not create a new CarsViewModel() per navigate because it won't save your colors on the Rectangle, instead you want to unbind the DataContent when you navigate away like so.

On CarsView.xaml / CarsView.xaml.cs create a Unloaded Event:

<phone:PhoneApplicationPage  Unloaded="PhoneApplicationPage_Unloaded">
private void PhoneApplicationPage_Unloaded(object sender, RoutedEventArgs e)
{
    this.DataContext = null;
}

Then you need to check for nulls on your Car Set function so it will not change your selection to null.

public Car SelectedCar
{
    get
    {
        System.Diagnostics.Debug.WriteLine("[CARSVIEWMMODEL] \t" + "GET SelectedCar");
        return selectedCar;
    }

    set
    {
        if (value != selectedCar && value != null)
        {
            selectedCar = value;
            System.Diagnostics.Debug.WriteLine("[CARSVIEWMMODEL] \t" + "SET SelectedCar");
            RaisePropertyChanged(() => SelectedCar);
       }
    }
}


If you apply the changes you will see it will act the way you want to, plus it will saved the color of that Rectangle when you navigate back to the Cars page.

Upvotes: 1

McGarnagle
McGarnagle

Reputation: 102793

It seems clear from what you describe that the "CarsView" instances are leaking -- when you navigate back in the app, they are not being disposed. That results in them hanging around as phantoms, still bound to the view-model, and thus triggering the conversion call whenever "SelectedCar" gets updated.

I wonder if your setup of the view-model locator is somehow faulty (I don't use this pattern, so I don't know). In theory, the XAML runtime uses weak event listeners for bindings, allowing the view to be disposed even if the view-model is still holding bindings. That may not be the case here, because the binding is applied to a StaticResource in the page ...

I would try this: use a transient scope for the view model instantiation. This prevents the IOC container from holding onto a reference to the view model, thus allowing it to be GC'd:

    public CarsViewModel CarsVM
    {
        get
        {
            return new CarsViewModel();
        }
    }

I realize that this breaks your IOC pattern, but if this fixes the problem, then at least you will know the proximate cause.

Edit

Another thing to try: put the locator on in the resources for the view model, instead of in the application resources. This might encourage the view to be disposed, since the static resource reference will be owned by the page.

<phone:PhoneApplicationPage.Resources>
    <vm:ViewModelLocator x:Key="Locator" d:IsDataSource="True" />
</phone:PhoneApplicationPage.Resources>

<Grid x:Name="LayoutRoot" DataContext="{Binding Source={StaticResource Locator},Path=CarsVM}">

Upvotes: 0

Related Questions