Below is the BindableProperty definition on a custom control:
public static BindableProperty ItemsSourceProperty =
BindableProperty.Create<NodeListView, IEnumerable<Node>> (ctrl =>
ctrl.ItemsSource,defaultValue: null,
defaultBindingMode: BindingMode.TwoWay,
propertyChanging: (bindable, oldValue, newValue) => {
var ctrl = (NodeListView)bindable;
ctrl.ItemsSource = newValue;
});
public IEnumerable<Node> ItemsSource {
get {
var itemsSource = (IEnumerable<Node>)GetValue (ItemsSourceProperty);
return itemsSource;
}
set {
SetValue (ItemsSourceProperty, value);
BindNodeIcons ();
}
}
When I set the BindingContext on the control I can see the newValue in propertyChanging is set to a correct object. Also in the setter of ItemsSource property the value variable takes the correct value. In the BindNodeIcons() method, I access ItemsSource property and it returns null. I can't see anything wrong in the code but still.
Try the following:
public class NodeListView : BindableObject
{
public static readonly BindableProperty ItemsSourceProperty = BindableProperty.Create(nameof(ItemsSource), typeof(IEnumerable<Node>), typeof(NodeListView), propertyChanged: OnItemSourceChanged);
public IEnumerable<Node> ItemsSource
{
get { return (IEnumerable<Node>)GetValue(ItemsSourceProperty); }
set { SetValue(ItemsSourceProperty, value); }
}
private static void OnItemSourceChanged(BindableObject bindable, object oldValue, object newValue)
{
var nodeListView = bindable as NodeListView;
nodeListView.BindNodeIcons();
}
}
Related
It may sound like a dumb question but why are bindable properties static?
public static readonly BindableProperty MapSpanProperty = BindableProperty.Create
(
propertyName: "MapSpan",
returnType: typeof(MapSpan),
declaringType: typeof(BindableMap),
defaultValue: null,
defaultBindingMode: BindingMode.TwoWay,
propertyChanged: MapSpanPropertyChanged
);
public MapSpan MapSpan
{
get
{
return (MapSpan)GetValue(MapSpanProperty);
}
set
{
SetValue(MapSpanProperty, value);
}
}
I have this code and it works just fine if I make the bindable property static, otherwise, it doesn't work. If I make this bindable property static it means, let's say if I have 2 maps opened at the same time, that this value will be the same on both if I set it on one of them?
Each bindable property has a corresponding public static readonly field of type BindableProperty that is exposed on the same class and that is the identifier of the bindable property.
You could check the source code of the control which provide the binable property.
I use the contentview for example. The code is from the link below. Xamarin forms update viewmodel field from a bindable property
public partial class MyCustomControl : ContentView
{
private string _text;
public string Text
{
get { return _text; }
set
{
_text = value;
OnPropertyChanged();
}
}
public static readonly BindableProperty TextProperty = BindableProperty.Create(
nameof(Text),
typeof(string),
typeof(MyCustomControl),
string.Empty,
propertyChanged: (bindable, oldValue, newValue) =>
{
var control = bindable as MyCustomControl;
//var changingFrom = oldValue as string;
//var changingTo = newValue as string;
control.Title.Text = newValue.ToString();
});
public MyCustomControl()
{
InitializeComponent();
}
The ContentView provide the public static readonly BindableProperty.
//
// Summary:
// An element that contains a single child element.
//
// Remarks:
// The following example shows how to construct a new ContentView with a Label inside.
[ContentProperty("Content")]
public class ContentView : TemplatedView
{
//
// Summary:
// Backing store for the Xamarin.Forms.ContentView.Content property..
//
// Remarks:
// To be added.
public static readonly BindableProperty ContentProperty;
public ContentView();
//
// Summary:
// Gets or sets the content of the ContentView.
public View Content { get; set; }
//
// Summary:
// Method that is called when the binding context changes.
//
// Remarks:
// To be added.
protected override void OnBindingContextChanged();
}
You could check the MS docs about more for the static in binable property. https://learn.microsoft.com/en-us/xamarin/xamarin-forms/xaml/bindable-properties
I created a bindableproperty in a customview but it can not get value.
here is the code:
public string BackgroundImage
{
get
{
var image = (image)GetValue(BackgroundImageProperty);
return image;
}
set => SetValue(BackgroundImageProperty, value);
}
the image is null.
public static readonly BindableProperty BackgroundImageProperty = BindableProperty.Create(nameof(BackgroundImage), typeof(string), typeof(MyView), null);
the xaml code:
<local:MyView BackgroundImage="back.png" />
I do want give a string value and give it to a Image.
I have give the BackgroundImage a string value in the xaml.
Solution1:
You can get the value from the event propertyChanged
public static readonly BindableProperty TextProperty = BindableProperty.Create(
nameof(Text),
typeof(string),
typeof(MyView),
null,
propertyChanged: (bindable, oldValue, newValue) =>
{
var value = newValue;
// do some thing you want .
}
);
Solution 2:
You can set the BindingContext in CustomView .
public MyView
{
//...
BindingContext = this;
//...
}
I have implemented the HVScrollView custom renderer in Xamarin.Forms as below and am trying to set the background colour of the item selected on tapped/clicked but can't figure out how to do that.
I can't figure out whether I need to add something to the custom renderer to make it show the selected item or whether it should be set in my markup.
Please help
public class HVScrollGridView : Grid
{
private ICommand _innerSelectedCommand;
private readonly ScrollView _scrollView;
private readonly StackLayout _itemsStackLayout;
public event EventHandler SelectedItemChanged;
public StackOrientation ListOrientation { get; set; }
public double Spacing { get; set; }
public static readonly BindableProperty SelectedCommandProperty =
BindableProperty.Create("SelectedCommand", typeof(ICommand), typeof(HVScrollGridView), null);
public static readonly BindableProperty ItemsSourceProperty =
BindableProperty.Create("ItemsSource", typeof(IEnumerable), typeof(HVScrollGridView), default(IEnumerable<object>), BindingMode.TwoWay, propertyChanged: ItemsSourceChanged);
public static readonly BindableProperty SelectedItemProperty =
BindableProperty.Create("SelectedItem", typeof(object), typeof(HVScrollGridView), null, BindingMode.TwoWay, propertyChanged: OnSelectedItemChanged);
public static readonly BindableProperty ItemTemplateProperty =
BindableProperty.Create("ItemTemplate", typeof(DataTemplate), typeof(HVScrollGridView), default(DataTemplate));
public ICommand SelectedCommand
{
get { return (ICommand)GetValue(SelectedCommandProperty); }
set { SetValue(SelectedCommandProperty, value); }
}
public IEnumerable ItemsSource
{
get { return (IEnumerable)GetValue(ItemsSourceProperty); }
set { SetValue(ItemsSourceProperty, value); }
}
public object SelectedItem
{
get { return (object)GetValue(SelectedItemProperty); }
set { SetValue(SelectedItemProperty, value); }
}
public DataTemplate ItemTemplate
{
get { return (DataTemplate)GetValue(ItemTemplateProperty); }
set { SetValue(ItemTemplateProperty, value); }
}
private static void ItemsSourceChanged(BindableObject bindable, object oldValue, object newValue)
{
var itemsLayout = (HVScrollGridView)bindable;
itemsLayout.SetItems();
}
public HVScrollGridView()
{
_scrollView = new ScrollView();
_itemsStackLayout = new StackLayout
{
BackgroundColor = BackgroundColor,
Padding = Padding,
Spacing = Spacing,
HorizontalOptions = LayoutOptions.FillAndExpand
};
_scrollView.BackgroundColor = BackgroundColor;
_scrollView.Content = _itemsStackLayout;
Children.Add(_scrollView);
}
protected virtual void SetItems()
{
_itemsStackLayout.Children.Clear();
_itemsStackLayout.Spacing = Spacing;
_innerSelectedCommand = new Command<View>(view =>
{
SelectedItem = view.BindingContext;
SelectedItem = null; // Allowing item second time selection
});
_itemsStackLayout.Orientation = ListOrientation;
_scrollView.Orientation = ListOrientation == StackOrientation.Horizontal
? ScrollOrientation.Horizontal
: ScrollOrientation.Vertical;
if (ItemsSource == null)
{
return;
}
foreach (var item in ItemsSource)
{
_itemsStackLayout.Children.Add(GetItemView(item));
}
_itemsStackLayout.BackgroundColor = BackgroundColor;
SelectedItem = null;
}
protected virtual View GetItemView(object item)
{
var content = ItemTemplate.CreateContent();
var view = content as View;
if (view == null)
{
return null;
}
view.BindingContext = item;
var gesture = new TapGestureRecognizer
{
Command = _innerSelectedCommand,
CommandParameter = view
};
AddGesture(view, gesture);
return view;
}
private void AddGesture(View view, TapGestureRecognizer gesture)
{
view.GestureRecognizers.Add(gesture);
var layout = view as Layout<View>;
if (layout == null)
{
return;
}
foreach (var child in layout.Children)
{
AddGesture(child, gesture);
}
}
private static void OnSelectedItemChanged(BindableObject bindable, object oldValue, object newValue)
{
var itemsView = (HVScrollGridView)bindable;
if (newValue == oldValue && newValue != null)
{
return;
}
itemsView.SelectedItemChanged?.Invoke(itemsView, EventArgs.Empty);
if (itemsView.SelectedCommand?.CanExecute(newValue) ?? false)
{
itemsView.SelectedCommand?.Execute(newValue);
}
}
public static explicit operator ListView(HVScrollGridView v)
{
throw new NotImplementedException();
}
}
Below is how I'm defining the property, it works for other types of property like string or bool but it won't work for the type Command:
public static readonly BindableProperty ClickedProperty = BindableProperty.Create(
nameof(Clicked),
typeof(Command),
typeof(MyCustomControl),
default(Command),
BindingMode.OneWay,
propertyChanging: (bindable, oldValue, newValue) => {
var control = bindable as MyCustomControl;
if (!control.GestureRecognizers.Contains(gesture))
{
control.GestureRecognizers.Add(gesture);
}
}
);
public Command Clicked
{
get { return (Command)GetValue(ClickedProperty); }
set { SetValue(ClickedProperty, value); }
}
I tried searching for how it's done for the class Xamarin.Forms.Button but it seems to use the IButtonController interface which is not for public use.
Update
I changed the property to event type, now the accessors are called when I bind the property from a XAML form, but neither propertyChanged nor propertyChanging is called.
public event EventHandler Clicked
{
add
{
lock (gesture)
{
gesture.Tapped += value;
}
}
remove
{
lock (gesture)
{
gesture.Tapped -= value;
}
}
}
pardon the cross-posting in the xamarin forum, but no one answers me there.
Some time ago I was looking for a Repeater-like control in XF, and I finally get this http://www.qimata.com/?p=7671, very simple indeed. I then started the usual "why don't add this, why don't add that" and so I added other properties and templates. Now, the control works very well for now, but I have a problem (apart from this, I don't think that is the best way to handle this scenario, if you have advice please share your thoughts).
All the logic is in the ItemsChanged event, that fires when ItemSource property is bound. Now, if I don't write the property for last, when event fires the other are yet to be evaluated. For example, this
<local:RepeaterView ShowSeparator="false" ItemsSource="{Binding itemsource}">
is not the same of
<local:RepeaterView ItemsSource="{Binding itemsource}" ShowSeparator="false">
Only in the first case property ShowSeparator has the expected value, because ItemsChanged fires before parameter initialization. Now, caring about parameters' order is not acceptable, then how can I handle this in a more decently manner?
public class RepeaterView : StackLayout
{
public static readonly BindableProperty ItemTemplateProperty = BindableProperty.Create(nameof(ItemTemplate), typeof(DataTemplate), typeof(RepeaterView), default(DataTemplate));
public static readonly BindableProperty HeaderTemplateProperty = BindableProperty.Create(nameof(HeaderTemplate), typeof(DataTemplate), typeof(RepeaterView), default(DataTemplate));
public static readonly BindableProperty EmptyTextTemplateProperty = BindableProperty.Create(nameof(EmptyTextTemplate), typeof(DataTemplate), typeof(RepeaterView), default(DataTemplate));
public static readonly BindableProperty ItemsSourceProperty = BindableProperty.Create(nameof(ItemsSource), typeof(ICollection), typeof(RepeaterView), new List<object>(), BindingMode.OneWay, null, propertyChanged: (bindable, oldValue, newValue) => { ItemsChanged(bindable, (ICollection)oldValue, (ICollection)newValue); });
public static readonly BindableProperty EmptyTextProperty = BindableProperty.Create(nameof(EmptyText), typeof(string), typeof(RepeaterView), string.Empty);
public static readonly BindableProperty SelectedItemCommandProperty = BindableProperty.Create(nameof(SelectedItemCommand), typeof(ICommand), typeof(RepeaterView), default(ICommand));
public ICollection ItemsSource
{
get { return (ICollection)GetValue(ItemsSourceProperty); }
set { SetValue(ItemsSourceProperty, value); }
}
public DataTemplate ItemTemplate
{
get { return (DataTemplate)GetValue(ItemTemplateProperty); }
set { SetValue(ItemTemplateProperty, value); }
}
public DataTemplate HeaderTemplate
{
get { return (DataTemplate)GetValue(HeaderTemplateProperty); }
set { SetValue(HeaderTemplateProperty, value); }
}
public DataTemplate EmptyTextTemplate
{
get { return (DataTemplate)GetValue(EmptyTextTemplateProperty); }
set { SetValue(EmptyTextTemplateProperty, value); }
}
public string EmptyText
{
get { return (string)GetValue(EmptyTextProperty); }
set { SetValue(EmptyTextProperty, value); }
}
public ICommand SelectedItemCommand
{
get { return (ICommand)GetValue(SelectedItemCommandProperty); }
set { SetValue(SelectedItemCommandProperty, value); }
}
public bool ShowSeparator { get; set; } = true;
private static void ItemsChanged(BindableObject bindable, ICollection oldValue, ICollection newValue)
{
var repeater = (RepeaterView)bindable;
repeater.Children.Clear();
var headerTemplate = repeater.HeaderTemplate;
if (headerTemplate != null)
{
var content = headerTemplate.CreateContent();
if (!(content is View) && !(content is ViewCell))
{
//throws exception
}
var view = (content is View) ? content as View : ((ViewCell)content).View;
repeater.Children.Add(view);
repeater.Children.Add(new Divider());
}
if (newValue.Count == 0 && (repeater.EmptyTextTemplate != null || !string.IsNullOrEmpty(repeater.EmptyText)))
{
if (repeater.EmptyTextTemplate == null)
repeater.Children.Add(new Label { Text = repeater.EmptyText });
else
{
var content = repeater.EmptyTextTemplate.CreateContent();
if (!(content is View) && !(content is ViewCell))
{
//throws exception
}
var view = (content is View) ? content as View : ((ViewCell)content).View;
repeater.Children.Add(view);
}
return;
}
var dataTemplate = repeater.ItemTemplate;
foreach (object item in newValue)
{
var content = dataTemplate.CreateContent();
if (!(content is View) && !(content is ViewCell))
{
//throws exception
}
var view = (content is View) ? content as View : ((ViewCell)content).View;
if (repeater.SelectedItemCommand != null)
{
var tapGestureRecognizer = new TapGestureRecognizer();
tapGestureRecognizer.Tapped += (sender, e) => { repeater.SelectedItemCommand?.Execute(item); };
view.GestureRecognizers.Add(tapGestureRecognizer);
}
view.BindingContext = item;
repeater.Children.Add(view);
if (repeater.ShowSeparator)
repeater.Children.Add(new Divider { Margin = new Thickness(5, 0) });
}
}
}
}
The best strategy here would be to make sure that the items are only calculated first, once they are really requested (like in LayoutChildren).
So in OnItemSourceChanged, you only set the ItemSource, but don't do anything further unless inizialization was already done.
Should look somewhat like this (pseude-code):
private static void ItemsChanged(...)
{
var repeater = (Repeaterview)bindable;
repeater.ItemsSource = value;
if(repeater.IsInitialized) UpdateItems();
}
private override void LayoutChildren()
{
IsInitialized = true;
UpdateItems();
}
This is the basic strategy. I'll update to the correct methods/overrides once I find the time to do so. Feel free to update this answer, if you happen to find out before me.