UWP Listview binding data template binding to page view model instead of ItemsSource class

EricWipperfurthEricWipperfurth USMember ✭✭
edited October 9 in Xamarin.Forms

I have a Xamarin forms project with Android and UWP targets. The Android version has been fully tested and released and works great. I'm using the latest versions of Xamarin (4.2.0.848062). I'm currently working on the UWP version and have run into an issue in the shared project. As stated, everything works fine on Android, but not in UWP. Here's the scenario and the pertinent information:

I have a viewmodel that derives from a class that has the following:
public ICommand IncrementCommand { get { // blah, blah, blah } }

The viewModel is bound to my page with the following xaml (only showing pertinent code).
Test

 <ListView ItemTapped="OnListViewTapped" 
        ItemsSource="{Binding CarriageMotorCfg}" 
        HasUnevenRows="true">

        <ListView.ItemTemplate>
          <DataTemplate>
            <ViewCell>
                            <StackLayout Orientation="Vertical">

                                    <local:ImageButton 
                        Source="{local:ImageResource EclipseDiag.images.plus.png}" Command="{Binding IncrementCommand}" CommandParameter="MotorCountString" Aspect="AspectFit" Margin="0" />

                                    <Entry Text="{Binding MotorCountString, Mode=TwoWay}" 
                            Placeholder="xxxx"
                            Keyboard="Numeric" 
                            VerticalOptions="Center"/>
...

CarriageMotorCfg is an ObservableCollection of a class that happens to contain the following (note, it is the same name as in the viewmodel above:
public ICommand IncrementCommand { get { // blah, blah, blah } }

When I try to load the page while running on UWP, the Binding of 'IncrementCommand' 'within the listview data template ends up getting mapped to the page's ViewModel instead of the class of CarriageMotorCfg. All of the other bindings in the template get mapped to the 'CarriageMotorCfg' class as desired. It is like the Xamarin UWP implementation is first checking the ViewModel for a match of 'IncrementCommand' before checking the class of 'CarriageMotorCfg'. If I change the name of the command within the 'CarriageMotorCfg' class to 'IncrementCommand2' and also change the XAML, then everything works as expected.

As stated above, this code works fine on Android so it seems to be this is a bug in Xamarin UWP. Any thoughts on fixing this without having to rename the commands?

Answers

  • LandLuLandLu Member, Xamarin Team Xamurai

    You'd better share your sample here.
    I created a simple demo depending on your description for testing the issue. It works fine. The IncrementCommand can be triggered in the single model class. And the same name command in view model won't be called.
    You can refer to my attachment for the specific code.

  • EricWipperfurthEricWipperfurth USMember ✭✭

    I've modified your project so it produces the error. Maybe the problem occurs when the ViewModel and the Model derive from the same base class?

  • EricWipperfurthEricWipperfurth USMember ✭✭

    Since my zip file didn't show up, below is your mainPage.Xaml.cs modified so it produces the error. It will throw an exception and if you inspect 'this' at the point of exception you will notice it is the ViewModel instead of the model.

    using System;
    using System.ComponentModel;
    using System.Runtime.CompilerServices;
    using System.Collections.Generic;
    using System.ComponentModel;
    using System.Linq;
    using System.Reflection;
    using System.Text;
    using System.Threading.Tasks;
    using System.Windows.Input;
    using Xamarin.Forms;
    
    namespace Sample
    {
        // Learn more about making custom code visible in the Xamarin.Forms previewer
        // by visiting https://aka.ms/xamarinforms-previewer
        [DesignTimeVisible(false)]
        public partial class MainPage : ContentPage
        {
            public MainPage()
            {
                InitializeComponent();
    
                BindingContext = new ViewModel();
            }
        }
    
        public class ViewModel:BaseModel
        {
            public List<Model> CarriageMotorCfg { set; get; }
            //public ICommand IncrementCommand { set; get; }
    
            public ViewModel()
            {
                List<Model> list = new List<Model>();
                for (int i=0; i<10; i++)
                {
                    list.Add(new Model { BtnImageSource = "draw.png" });
                }
                CarriageMotorCfg = list;
    
                //IncrementCommand = new Command<string>((parameter) =>
                //{
    
                //});
            }
        }
    
        public class Model:BaseModel
        {
            public Model()
            {
                //IncrementCommand = new Command<string>((parameter) =>
                //{
    
                //});
            }
    
            public string BtnImageSource { set; get; }
    
            public string MotorCountString { set; get; }
        }
    
        public class BaseModel: INotifyPropertyChanged
        {
            ICommand _incrementCommand;
    
            public event PropertyChangedEventHandler PropertyChanged;
    
            internal bool ProcPropertyChanged<T>(ref T currentValue, T newValue, [CallerMemberName] string propertyName = "")
            {
                return PropertyChanged.SetProperty(this, ref currentValue, newValue, propertyName);
            }
            internal void ProcPropertyChanged(string propertyName)
            {
                try
                {
                    // took out the ? for testing
                    PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
                }
                catch (Exception ex)
                {
                    System.Diagnostics.Debug.WriteLine(ex.ToString());
                    throw;
                }
            }
    
            public ICommand IncrementCommand
            {
                get
                {
                    return _incrementCommand ?? (
                        _incrementCommand = new RelayCommand((object prop) =>
    
                        {
                            String propertyName = (String)prop;
    
                            PropertyInfo pinfo = this.GetType().GetProperty(propertyName);
    
                            String currentValue = (String)pinfo.GetValue(this);
    
    
                            UInt16 temp = 0; // _propertyMinimums[propertyName];
                            if (UInt16.TryParse(currentValue, out temp))
                            {
                                //if (temp < _propertyMaximums[propertyName])
                                    temp++;
                            }
    
                            pinfo.SetValue(this, temp.ToString());
                        },
                        (object prop) =>
                        {
                            // Grab the property name
                            String propertyName = ((String)prop);
                            PropertyInfo pinfo = this.GetType().GetProperty(propertyName);
    
                            // All distances are bytes
                            String currentValue = (String)pinfo.GetValue(this);
    
                            UInt16 temp = 0; // _propertyMinimums[propertyName];
                            UInt16.TryParse(currentValue, out temp);
    
                            // Increment is enabled if cur < max
                            return true; // temp < _propertyMaximums[propertyName];
                        },
    
                                                                this));
            }
        }
    
    }
    
    public class RelayCommand : Command
    {
        public RelayCommand(Action<object> execute)
            : base(execute)
        {
        }
    
        public RelayCommand(Action execute)
            : this(o => execute())
        {
        }
    
        public RelayCommand(Action<object> execute, Func<object, bool> canExecute, INotifyPropertyChanged npc = null)
            : base(execute, canExecute)
        {
            if (npc != null)
                npc.PropertyChanged += delegate { ChangeCanExecute(); };
        }
    
        public RelayCommand(Action execute, Func<bool> canExecute, INotifyPropertyChanged npc = null)
            : this(o => execute(), o => canExecute(), npc)
        {
        }
    
        public void RaiseChangeCanExecute()
        {
            ChangeCanExecute();
        }
    }
    }
    
    namespace System.ComponentModel
    {
    public static class BaseNotify
    {
    
        public static bool SetProperty<T>(this PropertyChangedEventHandler handler, object sender, ref T currentValue, T newValue, [CallerMemberName] string propertyName = "")
        {
            if (EqualityComparer<T>.Default.Equals(currentValue, newValue))
                return false;
            currentValue = newValue;
            if (handler == null)
                return true;
            handler.Invoke(sender, new PropertyChangedEventArgs(propertyName));
            return true;
        }
    }
    }
    
  • LandLuLandLu Member, Xamarin Team Xamurai

    I've seen this issue. I've no idea why the canExecute function will be called to determine whether this command can be triggered. However, it only fires this canExecute function on UWP the real binding is set to the Model's IncrementCommand.
    So I think you could use the code below to get rid of the exception:

    (object prop) =>
    {
        // Grab the property name
        String propertyName = ((String)prop);
        if (this is Model)
        {
            PropertyInfo pinfo = this.GetType().GetProperty(propertyName);
    
            // All distances are bytes
            String currentValue = (String)pinfo.GetValue(this);
    
            UInt16 temp = 0; // _propertyMinimums[propertyName];
            UInt16.TryParse(currentValue, out temp);
            return true;
        }
        else
        {
            return false;
        }
    }, this));
    

    After this checking, you can make a breakpoint in the command's action and this is printing out to be Model in my case.

Sign In or Register to comment.