0
votes

While approaching Xamarin forms cross platform development, I'm struggling with the definitions of reusable controls.  As a first and very basic example, I've developed a dummy component which looks like this:

<ContentView xmlns="http://xamarin.com/schemas/2014/forms" 
             xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
             xmlns:d="http://xamarin.com/schemas/2014/forms/design"
             xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
             xmlns:vmBase="clr-namespace:TestApp.ViewModels.Base"
             mc:Ignorable="d"
             vmBase:ViewModelLocator.AutoWireViewModel="True"
             x:Class="TestApp.Views.Templates.HashtagContainerTemplateView"
             x:Name="this">
  <ContentView.Content>
        <StackLayout BindingContext="{Reference this}">
            <Label Text="{Binding Test}"/>
        </StackLayout>
    </ContentView.Content>
</ContentView>

Where the code behind is:

using Xamarin.Forms;
using Xamarin.Forms.Xaml;

namespace TestApp.Views.Templates
{
    [XamlCompilation(XamlCompilationOptions.Compile)]
    public partial class HashtagContainerTemplateView : ContentView
    {

        public static readonly BindableProperty TestProperty = BindableProperty.Create(
            propertyName: nameof(Test),
            returnType: typeof(string),
            declaringType: typeof(HashtagContainerTemplateView),
            defaultBindingMode: BindingMode.TwoWay,
            defaultValue: "I am the default value",
            propertyChanged: TestPropertyChanged);

        private static void TestPropertyChanged(BindableObject bindable, object oldValue, object newValue)
        {
            System.Diagnostics.Debugger.Break();    // This is called only when binding with constant values
        }

        public string Test
        {
            get => (string)GetValue(TestProperty);
            set
            {
                SetValue(TestProperty, value);
                System.Diagnostics.Debugger.Break();    // This is never called
            }
        }

        public HashtagContainerTemplateView()
        {
            InitializeComponent();
        }
    }
}

I am trying to load this view into a page by binding the Test property with a value set by the parent ViewModel as you can see here:

namespace TestApp.ViewModels
{
    public class MainViewModel : BaseViewModel
    {

        private string _testString;

        public string TestString 
        { 
            get => _testString; 
            set
            {
                _testString = value;
                RaisePropertyChanged();
            }
        }

        public MainViewModel()
        {
            
        }


        // This is called by the View Model Locator
        public override async Task InitializeAsync(object navigationData)
        {
            TestString = "I am the binded string";

            await base.InitializeAsync(navigationData);
        }
    }
}

Finally, the View is loaded into the parent Page as this:

<templates:HashtagContainerTemplateView Test="I am a costant string"/><!--This Works-->  
<templates:HashtagContainerTemplateView Test="{Binding TestString}"/> <!--Not Working-->

When running the App the labels displayed are:
I am a costant string which is as expected
I am the default value which is the default value for the Property, instead of the one I've passed through binding

After some debugging I realized that the TestPropertyChanged is called only when binding with constant values and the Test  setter is never called - see the breakpoints inside the code behind, right above - so I think this is the point... 

I know there are many topics like this, even here on SO, but I really can't make it work... I believe there is something really simple I'm missing ...

Final note: I am using the Microsoft eShopOnContainers project as a reference, hence I am using the View Model Locator approach. This is why the intialization is not in the ctor but in the InitializeAsync function.

Microsoft itself has a section about Content Views in the documentation, but no bindings are used...

3
Everything related to the content view binding looks correct. Did you try to bind the TestString property to a regular Label, just to check that the view model was correctly wired? Also it's not clear why do you need vmBase:ViewModelLocator.AutoWireViewModel="True" in your Content View?Valeriy Kovalenko
Did your problem resolved?Jack Hua
@ValeriyKovalenko the View Model Locator actually was the issue! Removing that, which was useless by the way, solved the problem.JTL

3 Answers

0
votes

You need to change the following:-

Text="{Binding Test,  Source={x:Reference this}}"

And remove the PropertyChanged to null.

0
votes

I use your code and it works well on my side. I guess you forget to set the BindingContext in the Parent Page.

Here is how I use it:

public partial class MainPage : ContentPage
{
    public MainPage()
    {
        InitializeComponent();

        BindingContext = new MainViewModel();
    }
}

public class MainViewModel : INotifyPropertyChanged
{
    public event PropertyChangedEventHandler PropertyChanged;

    private string _testString;

    public string TestString
    {
        get => _testString;
        set
        {
            _testString = value;
            if (PropertyChanged != null)
            {
                PropertyChanged(this, new PropertyChangedEventArgs("TestString"));
            }
        }
    }

    public MainViewModel()
    {
        TestString = "I am the binded string";
    }

}

And in Xaml:

<?xml version="1.0" encoding="utf-8" ?>
<ContentPage xmlns="http://xamarin.com/schemas/2014/forms"
             xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
             xmlns:d="http://xamarin.com/schemas/2014/forms/design"
             xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
             xmlns:templates="clr-namespace:App593"
             mc:Ignorable="d"
             x:Class="App593.MainPage">

    <StackLayout>
        <!-- Place new controls here -->
        <templates:View1 Test="I am a costant string"/>
        <templates:View1 Test="{Binding TestString}"/>
    </StackLayout>

</ContentPage>

Remember to add BindingContext = new MainViewModel(); in the Parent Page. Other codes in my project is exactly the same as yours.

0
votes

The problem was the View Model Locator, which was messing with the binding context and was just a leftover from a copy/paste.

Removing the vmBase:ViewModelLocator.AutoWireViewModel="True" from the Xaml solved the issue!

Thanks to @ValeriyKovalenko who made me spot it!