I have a view model, which is set as the binding context of my view.
I have a view which binds to one of the properties of the view model, which is a complex object.
I'm finding that the bindings are not firing for one of my properties "CurrentSpace" in the code below.
for now, I have to work around this, by manually setting it, but that's not gonna fly.
What could stop my controls getting values for bindings of this type? I breakpoint the getter, and there's no actual call to get any initial value from the CurrentSpace property - only changing them after the view is shown trigger the binding to lookup the getters.
i.e
<Label Text="{Binding TestText}" /> <Label x:Name="TestLabel" Text="{Binding CurrentSpace}" />
These bind to :
`
ISubscribedSpace _currentSpace;
public ISubscribedSpace CurrentSpace { get{ return _currentSpace; } set { SetProperty<ISubscribedSpace> (ref _currentSpace, value); } } string _testText; public string TestText { get{ return _testText; } set { SetProperty<string> (ref _testText, value); } }
`
I'm using ObservableObject (from xamarin forms labs for my viewmodels base class).
The CurrentSpace binding does nothing when the view appears. The TestText one works fine. If I update TestText, the label updates. If I update CurrentSpace, nothing happens.
Unsure why this is the case; but it's happening in all my view models. I wouldn't be surprised if this is another xamarin forms issue, though (I'll be ditchign it soon, some helpful stuff, but a lot of bugs I think).
The binding is working just fine. The problem is that you are misunderstanding how bindable properties work. When you use a bindable property the value is stored in a dictionary with the key being the BindableProperty
. The base class (BindableObject
) owns that dictionary. The binding system uses SetValue
and GetValue
to store and retrieve the value of bindable properties (passing in the BindableProperty
). Bindings do not go through your class property. This code:
public Space CurrentSpace { get { return (Space)GetValue(CurrentSpaceProperty); } set { SetValue(CurrentSpaceProperty, value); } }
is purely a convenience for setting and getting this property in C# code. Notice how it's just a wrapper around GetValue
and SetValue
. It's not used by the binding system. That code won't get called when the value is updated through the binding. The binding system goes directly to SetValue
.
What you want to do is respond to changes in the bindable property, and the way to do that is to use a callback that you pass in as one of the arguments of BindableProperty.Create
. This is how you should define your binding:
public static BindableProperty CurrentSpaceProperty = BindableProperty.Create( propertyName: "CurrentSpace", returnType: typeof(Space), declaringType: typeof(SpaceViewerLayout), defaultValue: null, defaultBindingMode: BindingMode.OneWay, propertyChanged: HandleCurrentSpaceChanged);
I labeled the arguments so it's clearer what they are, and I left out the ones with default values. Notice how I added a propertyChanged
argument. That method looks like this:
private static void HandleCurrentSpaceChanged(BindableObject bindable, object oldValue, object newValue) { var spaceLayout = (SpaceViewerLayout)bindable; var space = (Space)newValue; spaceLayout.BackgroundColor = space.Color; }
It's a static method because you have to pass it in as an argument to a constructor for a static field. Once you've done this you don't need to try to set the background color in the property setter itself. That code should just called SetValue
(as shown above), and that in turn causes this code to get called. The whole class now looks like this:
public class SpaceViewerLayout : AbsoluteLayout { public static BindableProperty CurrentSpaceProperty = BindableProperty.Create( propertyName: "CurrentSpace", returnType: typeof(Space), declaringType: typeof(SpaceViewerLayout), defaultValue: null, defaultBindingMode: BindingMode.OneWay, propertyChanged: HandleCurrentSpaceChanged); public Space CurrentSpace { get { return (Space)GetValue(CurrentSpaceProperty); } set { SetValue(CurrentSpaceProperty, value); } } private static void HandleCurrentSpaceChanged(BindableObject bindable, object oldValue, object newValue) { var spaceLayout = (SpaceViewerLayout)bindable; var space = (Space)newValue; spaceLayout.BackgroundColor = space.Color; } }
BoolTest is there, but nothing in your code actually sets it. If the default of the bindable property matches the value in the bound-to property then the value doesn't change, and therefore the "changed" callback isn't going to be called. If you change the default in the bindable property to true then you'll find that the callback does get called (the value changes from its default of true to the view model's value of false). Or, if you leave the bindable property default as false, but change the bound-to (view model) property to true then it will also get called (the value changes from the default of false to the view model's value of true).
As far as I can tell everything is working as expected. If you're seeing something else then you'll need to upload a better example and be clearer about what exactly you think is misbehaving.
Answers
further debugging. it looks like the bindingcontext is not being set on child views. why would that be?
CurrentSpace is not a string, but you're binding it to a string property. I think we covered in a previous post that there is no automatic string conversion, and in my testing this kind of binding would be silently ignored. You can use an IValueConverter to convert the object to a string.
thanks Adam; but I'm having the same thing for bindigns that match their type.
I have a custom control which has a CurrentSpace property; which also does nothing when the viewmodel property (of the same type) changes.
I have buttons binding their IsEnabled flag to boolean properties, they don't get fired either.
something seems seriously screwy to me.
Could you post an example project?
I think it's somehow the same problem. I can now see that my bindings are firing from the viewmodel; but it looks like I can't write a single control with a custom binding. In any case when I've tried to do this, the values don't get set on the actual target.
Not sure what else I can do..
Here's the code for my control that consumes the CurrentSpaces.
`
public class SpaceViewerLayout : AbsoluteLayout
{
public BindableProperty CurrentSpaceProperty = BindableProperty.Create ("CurrentSpace", typeof(ISubscribedSpace), typeof(SpaceViewerLayout), null, BindingMode.OneWay, null, null, null, null);
`
The CurrentSpaceProperty was static; but I made it a class level property, and now I'm back to the CurrentSpace mismatch can't convert Binding to ISubscribedSpace error - which I guess is progress, as at least it's trying to set something now.
If you could help me resolve this, I'd be very grateful, as you can probably imagine, this is pretty fundamental and is starting to effect me as my app is growing.
I've got total snow-blindness here : there's only so much I can keep reading the same lines of code and thinking; why does this work in other examples, but not in mine
The BindableProperty must be a public static field.
ok - when it's a public static field , it does nothing.
Is this fully documented anywhere? I find it incredible that every the bindings don't work on any single custom control I've written . I've spent maybe 6 hours this week simply debugging why bindings aren't firing, and I'm still in the same place - it's really holding me back from enjoying xamarin as much as I'd like to.
Post an example.
and here's some more info.. in my view (the SpaceViewerLayout)
`
`
The propertychange event is fired - every time currentspace changes. but that event is NEVER picked up by my CurrentSpaceLayout. This is the problem - I'm clearly doing something wrong - propertychange events are generated; but none of my custom controls ever pick them up in xaml - they don't even pick up initial values..
Any ideas on how I can debug this at least? It basically renders xamarin bindings useless for me, which really sucks as I'm enjoying xmarin a ton, and want to show it to other colleauges in my company; but not having binding workign will really reduce the sell..
I'll give you all of the classes involved. note, one of the bindings in the xaml, is a label - I know that won't work.. but the other one is the layout and should work.
I can't debug that. Post a working example I can just run.
Ok, here's a completely new project that demonstrates the problem using the same notions I'm struggling with in my app.
Ok, here's a completely new project that demonstrates the problem using the same notions I'm struggling with in my app.
I also updated the code for the layout to look like
`
using System;
using Xamarin.Forms;
namespace BindingTest
{
public class SpaceViewerLayout : AbsoluteLayout
{
// public static BindableProperty CurrentSpaceProperty = BindableProperty.Create ("CurrentSpace", typeof(Space), typeof(SpaceViewerLayout), null, BindingMode.OneWay, null, null, null, null);
public static readonly BindableProperty CurrentSpaceProperty = BindableProperty.Create<SpaceViewerLayout, Space> (p => p.CurrentSpace, null);
}
`
that code is straight from an online tutorial.. I have no idea why this isn't working now - I've been trying to get this simple kind of binding working for 8 hours now to no avail.. I honestly can't see what I'm doing wrong. Could you possibly post me an example of working binding that you have? My xamarin studio can't currently deploy to iphone, I'm starting to wonder if there's something up with my install; because I've run out of ideas at this point.
The binding is working just fine. The problem is that you are misunderstanding how bindable properties work. When you use a bindable property the value is stored in a dictionary with the key being the
BindableProperty
. The base class (BindableObject
) owns that dictionary. The binding system usesSetValue
andGetValue
to store and retrieve the value of bindable properties (passing in theBindableProperty
). Bindings do not go through your class property. This code:is purely a convenience for setting and getting this property in C# code. Notice how it's just a wrapper around
GetValue
andSetValue
. It's not used by the binding system. That code won't get called when the value is updated through the binding. The binding system goes directly toSetValue
.What you want to do is respond to changes in the bindable property, and the way to do that is to use a callback that you pass in as one of the arguments of
BindableProperty.Create
. This is how you should define your binding:I labeled the arguments so it's clearer what they are, and I left out the ones with default values. Notice how I added a
propertyChanged
argument. That method looks like this:It's a static method because you have to pass it in as an argument to a constructor for a static field. Once you've done this you don't need to try to set the background color in the property setter itself. That code should just called
SetValue
(as shown above), and that in turn causes this code to get called. The whole class now looks like this:Adam,
you're a tremendously helpful chap. I wish I could buy you a beer; but alas I live in Peru. Thanks for the explanation. I wish they would document this a bit better, as for n00bs who are having to ingest a lot of information pretty quickly this can be quite confusing.
Thanks again!
one last note on this,
how to debug issues? because I've understoot and implemented your changes; but there's one view where I still have issues.
I debugged, and noticed that the bindingcontext is null for one of the subviews. I suspect it's becuase this view is actually a custom page I created - in this case I have to set the bindingcontext on the controls which get skipped. but even doing that doesn't trigger their bindings. However, I've found that setting the property manually for the button does trigger the changehandler specified in the bindingproperty; so it appears that the mechanism works; but it still won't work for this button from xaml.
<localControls:ExtendedButton x:Name ="SpaceButton" ImageRoot="Images/Main/BottomBar/iconSpace" EnabledFlag="{Binding IsSpaceViewEnabled}" Clicked="OnSpaceViewerClicked" AbsoluteLayout.LayoutFlags="PositionProportional" AbsoluteLayout.LayoutBounds="0.5,0,44,44" ImageHeightRequest="44" ImageWidthRequest="44" />
from the ExtendedButton class:
public static BindableProperty EnabledFlagProperty = BindableProperty.Create ( propertyName: "EnabledFlag", returnType: typeof(bool), declaringType: typeof(ExtendedButton), defaultValue: false, defaultBindingMode: BindingMode.OneWay, propertyChanged: HandleEnabledFlagChanged); public bool EnabledFlag { get { return (bool)GetValue (EnabledFlagProperty); } set {SetValue (EnabledFlagProperty, value); }
private static void HandleEnabledFlagChanged (BindableObject bindable, object oldValue, object newValue) { var button = (ExtendedButton)bindable; button.IsEnabled = (bool)newValue; button.UpdateButtonImage (); }
As before, attach an example I can play with.
I already did. I don't know where the post went.
here it is again.
That project doesn't seem to match the code you quoted last (there is no ExtendedButton or EnabledFlag property). I don't know what I'm supposed to be looking at. Every binding I can see looks like it's working.
I removed the bits that weren't important. the one that's not working is the EnabledFlag on SpaceViewerLayout.
The property change handler is only ever invoked if I set it manually (i.e by directly setting the variable on the layout, not by changing the binding). I break point it, and I see it never get's invoked when the binding changes value.
The code you attached doesn't have an
EnabledFlag
property inSpaceViewerLayout
. Maybe you attached the wrong code.ok. sorry I'll fix it right now and upload it.
actually - I think it's the right code. it's not EnabledFlag, it's "BoolTest". Is that there?
that's now working - it doens't correspond to the value of the binding in SpaceViewerPage.xaml
BoolTest is there, but nothing in your code actually sets it. If the default of the bindable property matches the value in the bound-to property then the value doesn't change, and therefore the "changed" callback isn't going to be called. If you change the default in the bindable property to true then you'll find that the callback does get called (the value changes from its default of true to the view model's value of false). Or, if you leave the bindable property default as false, but change the bound-to (view model) property to true then it will also get called (the value changes from the default of false to the view model's value of true).
As far as I can tell everything is working as expected. If you're seeing something else then you'll need to upload a better example and be clearer about what exactly you think is misbehaving.
right you are. Is all of this actually documented anywhere? I've seen examples about bindings; but they don't state the need for static binding properties, matching the name, state that the getter and setter become nothing more than proxies, or that values which match the default will not generate events.
Perhaps this is common knowledge for c#/xaml developers; but all of this was news to me, and if it wasn't for your goodself I'd be absolutely non-the-wiser.
I don't know whether it's documented, but it generally works the same as other XAML-based platforms. Also, creating your own custom bindable properties is kind of an advanced feature that I think Xamarin expected that most Xamarin.Forms users would never need to do.
ok. I wonder if Xamarin realize what they're doing here. I'm hitting similar "Xamarin didn't think you'd do that" responses with my nested Pages. These are all fairly standard things for enterprise levlel apps.
I think my lack of xaml/.net expefdrience has held me back considerably here - looks like I should get myself a book on xaml, becuase Xamarin appear to borrow heavily from it.
Thanks for all your help!
I know its been a while, but the book Creating Mobile Apps With Xamarin.Forms has great examples of how to do this, and I'm sure Xamarin intended people to do this, its just an advanced concept that you'd have to be used to working with the platform at an advanced level.