Xamarin.Forms binding not updating after initial value
Asked Answered
I

3

7

I'm binding the title of my Xamarin.Forms.ContentPage to a property BuggyTitle in my view model (VM). The VM derives from MvxViewModel. Here's the simplified version:

BuggyPage.xaml:

<?xml version="1.0" encoding="UTF-8"?>
<local:ContentPage Title="{Binding BuggyTitle}"
            xmlns="http://xamarin.com/schemas/2014/forms" 
            xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml" 
            x:Class="MyProject.BuggyPage"
            xmlns:local="clr-namespace:Xamarin.Forms;assembly=MyProject">
<ContentPage.Content NavigationPage.HasNavigationBar="false">
        <Grid>
            <ScrollView>
                <!--and so on-->
</ContentPage.Content>
</local:ContentPage>

BuggyViewModel.cs:

namespace MyProject
{
    [ImplementPropertyChanged]
    public class BuggyViewModel : MvxViewModel
    {
      private Random _random;

      public string BuggyTitle {get; set;}

      public BuggyViewModel()
      {
          _random = new Random();
      }

      public override void Start()
        {
            base.Start();
            BuggyTitle = "" + _random.Next(1000);
            RaisePropertyChanged("BuggyTitle"); // this seems to make no difference
        }
    }
}

There's not much going on in the code behind other than a call to InitializeComponent() in the constructor.

The page is mapped to the VM generically in my project (not actually 'my' project, it's existing design), and it boils down to these (again, simplified) lines of code:

public static Page CreatePage(MvxViewModelRequest request)
{
    var viewModelName = request.ViewModelType.Name;
    var pageName = viewModelName.Replace ("ViewModel", "Page");
    var pageType = (typeof (MvxPagePresentationHelpers)).GetTypeInfo ().Assembly.CreatableTypes().FirstOrDefault(t => t.Name == pageName);
    var viewModelLoader = Mvx.Resolve<IMvxViewModelLoader>();
    var viewModel = viewModelLoader.LoadViewModel(request, null);
    var page = Activator.CreateInstance(pageType) as Page;
    page.BindingContext = viewModel;

   return page;
}

The problem:

When BuggyPage loads, I initially get the correct value for the title. Whenever it is displayed after that, even though I can see in the debugger that BuggyTitle is getting updated correctly, the change does not appear in the page.

Question:

Why don't updates to BuggyTitle get reflected in the page?

Edit 1:

To further describe the weirdness, I added a Label to my ContentPage, with x:Name="BuggyLabel" and Text="{Binding BuggyLabelText}". In my code-behind, I added this:

var binding_context = (BindingContext as BuggyViewModel);
if (binding_context != null)
{
    BuggyLabel.Text = binding_context.BuggyLabelText;
}

I set a breakpoint at BuggyLabel.Text =. It gets hit every time the page loads, and BuggyLabel.Text already seems to have the correct value (i.e, whatever binding_context.BuggyLabelText is set to). However, the actual page displayed only ever shows what the text in this label is initially set to.

And yes, have clean/built about a million times.

Edit 2 (further weirdness):

I put this in the code-behind so that it runs during page load:

var binding_context = (BindingContext as BuggyViewModel);
if (binding_context != null)
{
    Device.BeginInvokeOnMainThread(() =>
    {
        binding_context.RefreshTitleCommand.Execute(null);
    });
}

This again changes values in the debugger, but these changes don't get reflected in the displayed page.

I then added a button to the page and bound it to RefreshTitleCommand, and wham! the page updates its display.

Unfortunately I can't use this. Not only is it incredibly hackish, I can't have the user pressing buttons to have the page display what it's meant to on load.

I wonder if there's some caching going on with MvvmCross or Xamarin.

Istanbul answered 5/11, 2017 at 3:39 Comment(8)
Does BuggyTitle have a public field to get and set the property in?Grogan
Yes, sorry, forgot to add it in the question. I've edited the question.Istanbul
@Ash, Remove code-behind code. Not needed itMicroelectronics
Not the point I was trying to make. Yes I know I don't need it. Was trying to show how weird things are. The binding clearly is working, but the view simply refuses to display updates.Istanbul
as you have local:ContentPage, it probably does something wrong, but you haven't shown the code for that.Allistir
What code do I need to show? ContentPage belongs in the Xamarin.Forms namespace.Istanbul
does your binding work if you remove the code behind initial value? I guess, it does I have the same problem with the visibility for a very specific reason, i must use both binding and initial value in the code behind but when I have code behind binding doesnt work. Have you ever found a solution for this?Amorous
No it doesn't work. Tried every possible permutation, nothing works. I never managed to solve this, so rather just went around the problem.Istanbul
M
11

Answer

You need to add RaisePropertyChanged in BuggyTitle property declaration.

ViewModel

namespace MyProject
{
    [ImplementPropertyChanged]
    public class BuggyViewModel : MvxViewModel
    {
        private Random _random;

        string  _BuggyTitle { get; set; }

        public string BuggyTitle
        {
            get { return _BuggyTitle; }
            set { _BuggyTitle = value; RaisePropertyChanged(() => BuggyTitle); }
        }

        public BuggyViewModel()
        {
            _random = new Random();
        }

        public override void Start()
        {
            base.Start();
            BuggyTitle = "" + _random.Next(1000);
        }
    }
}

-----New Update------

Code behind code

var binding_context = (BindingContext as BuggyViewModel);
if (binding_context != null)
{
    Device.BeginInvokeOnMainThread(() =>
    {
        BuggyLabel.Text = binding_context.BuggyLabelText;
    });
}
Microelectronics answered 15/11, 2017 at 6:56 Comment(7)
Tried this also, followed by a clean/build. Still no luck :/ Have a look at the edit that I've made. I know that the binding is working fine because in the debugger I can see that the label's text property is always set to the correct value. However, it just doesn't reflect on the actual displayed page. Tried this on iOS simulator as well as physical device. Same issue on both.Istanbul
@Istanbul I have update code behind code. Check it and let me knowMicroelectronics
Ooh, can't wait to try this; seems promising. Anything main-thread-ish generally always seems to make UI things magically work. Will keep you posted.Istanbul
@Ash, add BuggyLabel.Text = binding_context.BuggyLabelText; in BeginInvokeOnMainThread methodMicroelectronics
That's what I tried before I included Edit 2, and it didn't work.Istanbul
@Ash, I think your ViewModel is not bonded with View. Check it with the set value of BuggyLabelText in declaring like public string BuggyTitle{get;set;}="Test"; and let me know.Microelectronics
If it's not bound, how do initial values appear fine? And how do values update fine when they are triggered by a user event such as a button click?Istanbul
C
2

I don't have any experience at all with Xamarin (but i do want to try it out in the future when i get as comfortable as possible with UWP), but i guess the Data Binding process should be working similar to what i am used to there ...

You are mentioning that you have no problem with the values that are set when the page first loads, however when you actually update the values there's no "linking" to the visual layer, despite at debug time you actually seeing the value being set to something completely different from it's initial state. Since you are dealing with properties-only viewmodel (Collections for instance in UWP are another level of events which need to be exposed), RaisePropertyChanged seems like the correct choice.

What i cannot understand is if when you first create your page, the Binding which you are creating is at least specified as One-Way mode, so changes in your viewmodel properties are propagated onto your UI when their set accessor methods are called.

You are setting your page context to viewmodel (each i figure is the same as DataContext in UWP/WPF), and therefore you can actually access those properties with the {Binding } markup. But what is the default mode for this operation in Xamarin ? (in UWP it is actually OneWay, and therefore it would work right of the bat for this situation ...). I have seen that in Xamarin it might be a bit different , since you also have the Default option. Can that be it?

PS. Hopefully this might be useful to you, despite my lack of experience with Xamarin.

Edit2 Implementing the INotifyPropertyChanged,

   public class BuggyViewModel : MvxViewModel, INotifyPropertyChanged
   {
        public event PropertyChangedEventHandler PropertyChanged;
        private Random _random;

        string  _BuggyTitle { get; set; }

        public string BuggyTitle
        {
            get { return _BuggyTitle; }
            set { _BuggyTitle = value; RaisePropertyChanged(() => 
                   BuggyTitle); }
        }

        public BuggyViewModel()
        {
            _random = new Random();
        }

        public override void Start()
        {
            base.Start();
            BuggyTitle = "" + _random.Next(1000);
        }


        protected void OnPropertyChanged(string propertyName)
        {
           var handler = PropertyChanged;
           if (handler != null)
              handler(this, new PropertyChangedEventArgs(propertyName));
        }
   }
Connotative answered 19/11, 2017 at 23:49 Comment(10)
The binding direction isn't the issue. Changes from VM are propagating through to the View properties fine, just not displayed.Istanbul
Event if at debug time, by inspecting your property binding in the View you see the property with the updated value, i don't think you can actually conclude that the view is actually binded to that value.Droppings
So how does it update in the display when I press a button that is bound to a command in the VM that updates the VM property in question? Ref: Edit 2.Istanbul
It might actually still have the previous property's value, if the binding mode is actually not set properly, or you are not raising the event properly (the event should clearly identify the property which has changed).Droppings
@Istanbul If the binding is not working, it is normal that after you set your property to a new value you are going to see it displayed in the console, but not on the view.Droppings
That means that much likely something is wrong between your VM and the View connection. Just to get this out of the way, have you tried this: Text="{Binding BuggyLabelText, Mode=OneWay}" ?Droppings
I'll put that in explicitly and get back.Istanbul
Nope. Didn't work. I've got this line that I'm examining in the debugger: RaisePropertyChanged(() => BuggyTitle);. In the debugger, BuggyTitle has the correct (updated) value, but the page displayed once again shows the old (initial) value.Istanbul
Instead of using the RaisePropertyChanged, try to check out the INotifyPropertyChanged Interface and have your class inherit from it. This class exposes the public event PropertyChangedEventHandler PropertyChanged, which notifies that certain property has changed.Droppings
Well I'm using MvvmCross which means I'm restricted to its navigation system. So I can't just implement INotifyPropertyChanged, and do things the usual way. Instead, I've tried implementing IMvxNotifyPropertyChanged, and RaisePropertyChanged is a helper as per the MvvmCross docs which itself makes use of PropertyChanged.Istanbul
L
0

I was setting a controls binding context from the property changed event of one of its properties.

This made my control stop tracking changes despite everything else binding correctly still, and the control would also correctly bind initially (first time is fine, further changes do not fire the property changed event again).

page.BindingContext = viewModel;
Laic answered 26/5, 2022 at 8:29 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.