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
    edited November 11

    It's best to 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.

  • EricWipperfurthEricWipperfurth USMember ✭✭

    Your proposed solution won't work. The ViewModel and the Model both inherit from BaseModel. The Increment/Decrement commands were put in the BaseModel so they can be used by both the ViewModel and the Model. Thus adding
    if (this is Model)
    will cause my calls to Increment/Decrement from the ViewModel to fail.

    This Xamarin bug stinks and should be fixed. My work around (due to time constraints) was to rename the Increment/Decrement commands in the model to avoid this "collision" on UWP.

    public ICommand DecrementCommand2
    {
        get
        {
            return DecrementCommand;
        }
    }
    
    public ICommand IncrementCommand2
    {
        get
        {
            return IncrementCommand;
        }
    }
    
  • Hi EricWipperfurth,
    Thanks for reaching out to us here. I'm sorry to hear about the issue with hiding properties, but glad to hear you were working around the problem meanwhile. I'm guessing the code that you commented out was necessary for the repro, and I see that you were trying to redefine the IncrementCommand in a child class that was already defined in the base class. I do see some issues in the code samples given. The fact this works in the Android version is likely due to Java or Android implementations. Per the C# specification though, it's incorrect.
    I'm sharing the solution and a side note about modifiers. Since the modifiers aren't required in the solution, I wanted to keep the topics separate.

    Solution

    You won't need to declare the properties again in the child classes. You should just set the IncrementCommand property value from the derived class, without declaring the property again. To do this, you'll need a protected setter for the IncrementCommand as shown:

    public ICommand IncrementCommand
    {
        get
        {
            //...
        }
        protected set
        {
            // ...
            _incrementCommand = value;
        }
    }
    

    Now, all you need in the base classes is the code to set the IncrementCommand. It would be similar to the following:

    public Model()
    {
        IncrementCommand = new Command<string>((parameter) =>
        {
            Debug.WriteLine($"executed command which was defined in: {nameof(Model)} constructor");
        });
    }
    

    Aside regarding modifiers: override, virtual, abstract, new

    As an aside, the way you were trying to declare the properties in the child class, with the same name as the base class, is called hiding.
    e.g. BaseModel class already declares a property called IncrementCommand. ViewModel extends BaseModel and ViewModel also declares a property called IncrementCommand. This is 'hiding'

    When you hide a member, you'll see a warning in the code editor and build output stating, "'ViewModel.IncrementCommand' hides inherited member 'BaseModel.IncrementCommand'. Use the new keyword if hiding was intended."

    The more common preferred alternative over hiding would be to use the override keyword in the derived classes, and in the base class you'd decorate the property with virtual or make it abstract.
    Hiding members without using the new keyword produces a warning displayed in the editor, and in the build output, that the new keyword should be used in this scenario. However the better option over hiding is generally to have a virtual in the base class and an override in the derived classes. Since all we need is a setter in the base class for your case, it looks like you won't need to use any of this (virtual, override, abstract, new) for setting the IncrementCommand.

Sign In or Register to comment.