Set background colour of HVScrollView item on click - xamarin.forms

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();
}
}

Related

Xamarin Forms Custom GridView Tap and Long Tap Not Working Together

I want to use tap and long tap together with Custom Gridview.
Tap is working but long tap not working.
it also works when one tap turns off the long tap.
Please help me.
Thank you.
public class GridView : Grid
{
public static readonly BindableProperty ItemsSourceProperty = BindableProperty.Create(nameof(ItemsSource), typeof(IList), typeof(GridView), default(IList), BindingMode.TwoWay);
public static readonly BindableProperty ItemTappedCommandProperty = BindableProperty.Create(nameof(ItemTappedCommand), typeof(ICommand), typeof(GridView), null);
public static readonly BindableProperty ItemLongTappedCommandProperty = BindableProperty.Create(nameof(ItemLongTappedCommand), typeof(ICommand), typeof(GridView), null);
public static readonly BindableProperty ItemTemplateProperty = BindableProperty.Create(nameof(ItemTemplate), typeof(DataTemplate), typeof(GridView), default(DataTemplate));
public static readonly BindableProperty MaxColumnsProperty = BindableProperty.Create(nameof(MaxColumns), typeof(int), typeof(GridView), 2);
public static readonly BindableProperty TileHeightProperty = BindableProperty.Create(nameof(TileHeight), typeof(float), typeof(GridView), 220f);//adjusted here reuired height
public GridView()
{
PropertyChanged += GridView_PropertyChanged;
PropertyChanging += GridView_PropertyChanging;
}
public IList ItemsSource
{
get { return (IList)GetValue(ItemsSourceProperty); }
set { SetValue(ItemsSourceProperty, value); }
}
public ICommand ItemTappedCommand
{
get { return (ICommand)GetValue(ItemTappedCommandProperty); }
set { SetValue(ItemTappedCommandProperty, value); }
}
public ICommand ItemLongTappedCommand
{
get { return (ICommand)GetValue(ItemLongTappedCommandProperty); }
set { SetValue(ItemLongTappedCommandProperty, value); }
}
public DataTemplate ItemTemplate
{
get { return (DataTemplate)GetValue(ItemTemplateProperty); }
set { SetValue(ItemTemplateProperty, value); }
}
public int MaxColumns
{
get { return (int)GetValue(MaxColumnsProperty); }
set { SetValue(MaxColumnsProperty, value); }
}
public float TileHeight
{
get { return (float)GetValue(TileHeightProperty); }
set { SetValue(TileHeightProperty, value); }
}
private void BuildColumns()
{
ColumnDefinitions.Clear();
for (var i = 0; i < MaxColumns; i++)
{
ColumnDefinitions.Add(new ColumnDefinition { Width = new GridLength(1, GridUnitType.Star) });
}
}
private View BuildTile(object item1)
{
var template = ItemTemplate.CreateContent() as View;
template.BindingContext = item1;
if (ItemTappedCommand != null)
{
var tapGestureRecognizer = new TapGestureRecognizer
{
Command = ItemTappedCommand,
CommandParameter = item1,
};
template.GestureRecognizers.Add(tapGestureRecognizer);
}
// Tap komutu eziyor.
if (ItemLongTappedCommand != null)
{
template.Effects.Add(new LongPressedEffect());
LongPressedEffect.SetCommand(template, ItemLongTappedCommand);
//LongPressedEffect.SetCommandParameter(template, item1);
}
return template;
}
private void BuildTiles()
{
// Wipe out the previous row & Column definitions if they're there.
if (RowDefinitions.Any())
{
RowDefinitions.Clear();
}
BuildColumns();
Children.Clear();
var tiles = ItemsSource;
if (tiles != null)
{
var numberOfRows = Math.Ceiling(tiles.Count / (float)MaxColumns);
for (var i = 0; i < numberOfRows; i++)
{
RowDefinitions.Add(new RowDefinition { Height = new GridLength(0, GridUnitType.Auto) });
}
for (var index = 0; index < tiles.Count; index++)
{
var column = index % MaxColumns;
var row = (int)Math.Floor(index / (float)MaxColumns);
var tile = BuildTile(tiles[index]);
Children.Add(tile, column, row);
}
}
}
private void GridView_PropertyChanged(object sender, PropertyChangedEventArgs e)
{
if (e.PropertyName == ItemsSourceProperty.PropertyName)
{
var items = ItemsSource as INotifyCollectionChanged;
if (items != null)
items.CollectionChanged += ItemsCollectionChanged;
BuildTiles();
}
if (e.PropertyName == MaxColumnsProperty.PropertyName || e.PropertyName == TileHeightProperty.PropertyName)
{
BuildTiles();
}
}
private void GridView_PropertyChanging(object sender, Xamarin.Forms.PropertyChangingEventArgs e)
{
if (e.PropertyName == ItemsSourceProperty.PropertyName)
{
var items = ItemsSource as INotifyCollectionChanged;
if (items != null)
items.CollectionChanged -= ItemsCollectionChanged;
}
}
private void ItemsCollectionChanged(object sender, NotifyCollectionChangedEventArgs e)
{
BuildTiles();
}
}
public class LongPressedEffect : RoutingEffect
{
public LongPressedEffect() : base("MyApp.LongPressedEffect")
{ }
public static readonly BindableProperty CommandProperty = BindableProperty.CreateAttached("Command", typeof(ICommand), typeof(LongPressedEffect), (object)null);
public static ICommand GetCommand(BindableObject view)
{
//do something you want
Console.WriteLine("long press Gesture recognizer has been striked");
return (ICommand)view.GetValue(CommandProperty);
}
public static void SetCommand(BindableObject view, ICommand value)
{
view.SetValue(CommandProperty, value);
}
public static readonly BindableProperty CommandParameterProperty = BindableProperty.CreateAttached("CommandParameter", typeof(object), typeof(LongPressedEffect), (object)null);
public static object GetCommandParameter(BindableObject view)
{
return view.GetValue(CommandParameterProperty);
}
public static void SetCommandParameter(BindableObject view, object value)
{
view.SetValue(CommandParameterProperty, value);
}
}
I noticed you used Effect to create your own long pressed command. But if you consumed a TapGestureRecognizer at the same time, it will intercept the effect. Then your long pressed command won't be triggered.
You can either define the single tap click event in the effect to achieve them both. Here is my effect:
public class PressedEffect : RoutingEffect
{
public PressedEffect() : base("MyApp.PressedEffect")
{
}
public static readonly BindableProperty LongTapCommandProperty = BindableProperty.CreateAttached("LongTapCommand", typeof(ICommand), typeof(PressedEffect), (object)null);
public static ICommand GetLongTapCommand(BindableObject view)
{
return (ICommand)view.GetValue(LongTapCommandProperty);
}
public static void SetLongTapCommand(BindableObject view, ICommand value)
{
view.SetValue(LongTapCommandProperty, value);
}
public static readonly BindableProperty LongParameterProperty = BindableProperty.CreateAttached("LongParameter", typeof(object), typeof(PressedEffect), (object)null);
public static object GetLongParameter(BindableObject view)
{
return view.GetValue(LongParameterProperty);
}
public static void SetLongParameter(BindableObject view, object value)
{
view.SetValue(LongParameterProperty, value);
}
public static readonly BindableProperty TapCommandProperty = BindableProperty.CreateAttached("TapCommand", typeof(ICommand), typeof(PressedEffect), (object)null);
public static ICommand GetTapCommand(BindableObject view)
{
return (ICommand)view.GetValue(TapCommandProperty);
}
public static void SetTapCommand(BindableObject view, ICommand value)
{
view.SetValue(TapCommandProperty, value);
}
public static readonly BindableProperty TapParameterProperty = BindableProperty.CreateAttached("TapParameter", typeof(object), typeof(PressedEffect), (object)null);
public static object GetTapParameter(BindableObject view)
{
return view.GetValue(TapParameterProperty);
}
public static void SetTapParameter(BindableObject view, object value)
{
view.SetValue(TapParameterProperty, value);
}
}
Android implementation:
[assembly: ResolutionGroupName("MyApp")]
[assembly: ExportEffect(typeof(AndroidPressedEffect), "PressedEffect")]
namespace PressedEffectDemo.Droid
{
public class AndroidPressedEffect : PlatformEffect
{
private bool _attached;
public static void Initialize() { }
public AndroidPressedEffect()
{
}
protected override void OnAttached()
{
if (!_attached)
{
if (Control != null)
{
Control.LongClickable = true;
Control.LongClick += Control_LongClick;
Control.Click += Control_Click;
}
else
{
Container.LongClickable = true;
Container.LongClick += Control_LongClick;
Container.Click += Control_Click;
}
_attached = true;
}
}
private void Control_Click(object sender, EventArgs e)
{
var command = PressedEffect.GetTapCommand(Element);
command?.Execute(PressedEffect.GetTapParameter(Element));
}
private void Control_LongClick(object sender, Android.Views.View.LongClickEventArgs e)
{
var command = PressedEffect.GetLongTapCommand(Element);
command?.Execute(PressedEffect.GetLongParameter(Element));
}
protected override void OnDetached()
{
if (_attached)
{
if (Control != null)
{
Control.LongClickable = true;
Control.LongClick -= Control_LongClick;
Control.Click -= Control_Click;
}
else
{
Container.LongClickable = true;
Container.LongClick -= Control_LongClick;
Control.Click -= Control_Click;
}
_attached = false;
}
}
}
}
iOS implementation:
[assembly: ResolutionGroupName("MyApp")]
[assembly: ExportEffect(typeof(iOSPressedEffect), "PressedEffect")]
namespace PressedEffectDemo.iOS
{
public class iOSPressedEffect : PlatformEffect
{
private bool _attached;
private readonly UILongPressGestureRecognizer _longPressRecognizer;
private readonly UITapGestureRecognizer _tapRecognizer;
public iOSPressedEffect()
{
_longPressRecognizer = new UILongPressGestureRecognizer(HandleLongClick);
_tapRecognizer = new UITapGestureRecognizer(HandleClick);
}
protected override void OnAttached()
{
if (!_attached)
{
Container.AddGestureRecognizer(_longPressRecognizer);
Container.AddGestureRecognizer(_tapRecognizer);
_attached = true;
}
}
private void HandleClick()
{
var command = PressedEffect.GetTapCommand(Element);
command?.Execute(PressedEffect.GetTapParameter(Element));
}
private void HandleLongClick(UILongPressGestureRecognizer recognizer)
{
if (recognizer.State == UIGestureRecognizerState.Ended)
{
var command = PressedEffect.GetLongTapCommand(Element);
command?.Execute(PressedEffect.GetLongParameter(Element));
}
}
protected override void OnDetached()
{
if (_attached)
{
Container.RemoveGestureRecognizer(_longPressRecognizer);
Container.RemoveGestureRecognizer(_tapRecognizer);
_attached = false;
}
}
}
}
At last, you can consume them on XAML like:
<StackLayout>
<Grid HeightRequest="200"
BackgroundColor="Green"
local:PressedEffect.TapCommand="{Binding TapCommand}"
local:PressedEffect.LongTapCommand="{Binding LongTapCommand}">
<Grid.Effects>
<local:PressedEffect />
</Grid.Effects>
</Grid>
</StackLayout>
You could refer to my sample here.

How to add swiping dots to Tabbed/Carousel page?

I'm wondering how to add dots indicating to swipe screens in either TabbedPage or CarouselPage like in the below image?
I tried adding images for that but they don't look natural so is there a real way for doing that?
My above workaround explanation in an example with 3 page:
I create 3 images each image has 3 dots one of them is highlighted:
First image highlighted dot is the first one.
Second image highlighted dot is the second one.
and etc.
you can use Xamarin.Forms.CarouselView and write a user control for page indicators. Follow the steps below,
Using Package Console, Install-Package Xamarin.Forms.CarouselView -Version 2.3.0-pre2 (Xamarin.Forms.CarouselView) package from NuGet in all 3 projects (PCL, iOS and Android).
add reference to Carousel view in the page directives,
xmlns:cv="clr-namespace:Xamarin.Forms;assembly=Xamarin.Forms.CarouselView"
and the Xaml code as below,
<StackLayout Padding="0,0,0,5" BackgroundColor="#d8d8d8" >
<cv:CarouselView x:Name="cview" ItemsSource="{Binding DataSource}" Position="{Binding Position, Mode=TwoWay}">
<cv:CarouselView.ItemTemplate>
<DataTemplate>
<Image Aspect="AspectFill" HorizontalOptions="Center" VerticalOptions="Center" Source="{Binding PickedImage}" />
</DataTemplate>
</cv:CarouselView.ItemTemplate>
</cv:CarouselView>
<cutomControl:CarouselIndicators IndicatorHeight="16" IndicatorWidth="16" UnselectedIndicator="unselected_circle.png" SelectedIndicator="selected_circle.png" Position="{Binding Position}" ItemsSource="{Binding DataSource}" />
</StackLayout>
notice, Position and your viewmodel should have,
private int _position;
public int Position
{
get { return _position; }
set
{
_position = value;
OnPropertyChanged();
}
}
notice, customControl below CarouselView.. Yes, you need to write a custom control for it. Just use the below custom control code and add reference in the page directive,
so your page directive will be as below,
xmlns:cutomControl="clr-namespace:XXXX.CustomControls;assembly=XXXX"
xmlns:cv="clr-namespace:Xamarin.Forms;assembly=Xamarin.Forms.CarouselView"
and the custom control code is,
public class CarouselIndicators : Grid
{
private ImageSource UnselectedImageSource = null;
private ImageSource SelectedImageSource = null;
private readonly StackLayout _indicators = new StackLayout() { Orientation = StackOrientation.Horizontal, HorizontalOptions = LayoutOptions.CenterAndExpand };
public CarouselIndicators()
{
this.HorizontalOptions = LayoutOptions.CenterAndExpand;
this.RowDefinitions.Add(new RowDefinition() { Height = GridLength.Auto });
this.Children.Add(_indicators);
}
public static readonly BindableProperty PositionProperty = BindableProperty.Create(nameof(Position), typeof(int), typeof(CarouselIndicators), 0, BindingMode.TwoWay, propertyChanging: PositionChanging);
public static readonly BindableProperty ItemsSourceProperty = BindableProperty.Create(nameof(ItemsSource), typeof(IEnumerable), typeof(CarouselIndicators), Enumerable.Empty<object>(), BindingMode.OneWay, propertyChanged: ItemsChanged);
public static readonly BindableProperty SelectedIndicatorProperty = BindableProperty.Create(nameof(SelectedIndicator), typeof(string), typeof(CarouselIndicators), "", BindingMode.OneWay);
public static readonly BindableProperty UnselectedIndicatorProperty = BindableProperty.Create(nameof(UnselectedIndicator), typeof(string), typeof(CarouselIndicators), "", BindingMode.OneWay);
public static readonly BindableProperty IndicatorWidthProperty = BindableProperty.Create(nameof(IndicatorWidth), typeof(double), typeof(CarouselIndicators), 0.0, BindingMode.OneWay);
public static readonly BindableProperty IndicatorHeightProperty = BindableProperty.Create(nameof(IndicatorHeight), typeof(double), typeof(CarouselIndicators), 0.0, BindingMode.OneWay);
public string SelectedIndicator
{
get { return (string)this.GetValue(SelectedIndicatorProperty); }
set { this.SetValue(SelectedIndicatorProperty, value); }
}
public string UnselectedIndicator
{
get { return (string)this.GetValue(UnselectedIndicatorProperty); }
set { this.SetValue(UnselectedIndicatorProperty, value); }
}
public double IndicatorWidth
{
get { return (double)this.GetValue(IndicatorWidthProperty); }
set { this.SetValue(IndicatorWidthProperty, value); }
}
public double IndicatorHeight
{
get { return (double)this.GetValue(IndicatorHeightProperty); }
set { this.SetValue(IndicatorHeightProperty, value); }
}
public int Position
{
get { return (int)this.GetValue(PositionProperty); }
set { this.SetValue(PositionProperty, value); }
}
public IEnumerable ItemsSource
{
get { return (IEnumerable)this.GetValue(ItemsSourceProperty); }
set { this.SetValue(ItemsSourceProperty, (object)value); }
}
private void Clear()
{
_indicators.Children.Clear();
}
private void Init(int position)
{
if (UnselectedImageSource == null)
UnselectedImageSource = ImageSource.FromFile(UnselectedIndicator);
if (SelectedImageSource == null)
SelectedImageSource = ImageSource.FromFile(SelectedIndicator);
if (_indicators.Children.Count > 0)
{
for (int i = 0; i < _indicators.Children.Count; i++)
{
if (((Image)_indicators.Children[i]).ClassId == nameof(State.Selected) && i != position)
_indicators.Children[i] = BuildImage(State.Unselected, i);
else if (((Image)_indicators.Children[i]).ClassId == nameof(State.Unselected) && i == position)
_indicators.Children[i] = BuildImage(State.Selected, i);
}
}
else
{
var enumerator = ItemsSource.GetEnumerator();
int count = 0;
while (enumerator.MoveNext())
{
Image image = null;
if (position == count)
image = BuildImage(State.Selected, count);
else
image = BuildImage(State.Unselected, count);
_indicators.Children.Add(image);
count++;
}
}
}
private Image BuildImage(State state, int position)
{
var image = new Image()
{
WidthRequest = IndicatorWidth,
HeightRequest = IndicatorHeight,
ClassId = state.ToString()
};
switch (state)
{
case State.Selected:
image.Source = SelectedImageSource;
break;
case State.Unselected:
image.Source = UnselectedImageSource;
break;
default:
throw new Exception("Invalid state selected");
}
image.GestureRecognizers.Add(new TapGestureRecognizer() { Command = new Command(() => { Position = position; }) });
return image;
}
private static void PositionChanging(object bindable, object oldValue, object newValue)
{
var carouselIndicators = bindable as CarouselIndicators;
carouselIndicators.Init(Convert.ToInt32(newValue));
}
private static void ItemsChanged(object bindable, object oldValue, object newValue)
{
var carouselIndicators = bindable as CarouselIndicators;
carouselIndicators.Clear();
carouselIndicators.Init(0);
}
public enum State
{
Selected,
Unselected
}
}

Custom Control parameters binding order

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.

How to render a xamarin.forms view in a custom renderer

I've got the following code:
Shared project:
using System;
using Xamarin.Forms;
namespace XamarinForms.Framework.Controls
{
public enum DrawerPosition
{
Left,
Right
}
public interface ISideDrawerNativeEventProxy
{
void RaiseSlideChanged(float percentage);
}
public class SideDrawer : Grid, ISideDrawerNativeEventProxy
{
#region DrawerSize
public static BindableProperty DrawerSizeProperty = BindableProperty.Create<SideDrawer, MaxedPercentSize>(d => d.DrawerSize, new MaxedPercentSize(MaxedPercentSizeType.Width, 80, 400));
public MaxedPercentSize DrawerSize
{
get { return (MaxedPercentSize) GetValue(DrawerSizeProperty); }
set { SetValue(DrawerSizeProperty, value); }
}
#endregion DrawerSize
#region IsOpen
public static BindableProperty IsOpenProperty = BindableProperty.Create<SideDrawer, bool>(d => d.IsOpen, default(bool));
public bool IsOpen
{
get { return (bool) GetValue(IsOpenProperty); }
set { SetValue(IsOpenProperty, value); }
}
#endregion IsOpen
#region DrawerPosition
public static BindableProperty DrawerPositionProperty = BindableProperty.Create<SideDrawer, DrawerPosition>(d => d.DrawerPosition, default(DrawerPosition));
public DrawerPosition DrawerPosition
{
get { return (DrawerPosition) GetValue(DrawerPositionProperty); }
set { SetValue(DrawerPositionProperty, value); }
}
#endregion DrawerPosition
public static BindableProperty MainContentProperty = BindableProperty.Create<SideDrawer, View>(d => d.MainContent, default(View));
public View MainContent
{
get { return (View) GetValue(MainContentProperty); }
set { SetValue(MainContentProperty, value); }
}
public static BindableProperty DrawerContentProperty = BindableProperty.Create<SideDrawer, View>(d => d.DrawerContent, default(View));
public View DrawerContent
{
get { return (View) GetValue(DrawerContentProperty); }
set { SetValue(DrawerContentProperty, value); }
}
#region DrawerLength
public static BindableProperty DrawerLengthProperty = BindableProperty.Create<SideDrawer, int>(d => d.DrawerLength, default(int), defaultValueCreator: DrawerLengthDefault);
private static int DrawerLengthDefault(SideDrawer bindable)
{
return 300;
}
public int DrawerLength
{
get { return (int) GetValue(DrawerLengthProperty); }
set { SetValue(DrawerLengthProperty, value); }
}
#endregion DrawerLength
#region IsContentTranslated
public static BindableProperty IsContentTranslatedProperty = BindableProperty.Create<SideDrawer, bool>(d => d.IsContentTranslated, true);
public bool IsContentTranslated
{
get { return (bool) GetValue(IsContentTranslatedProperty); }
set { SetValue(IsContentTranslatedProperty, value); }
}
#endregion IsContentTranslated
void ISideDrawerNativeEventProxy.RaiseSlideChanged(float percentage)
{
}
}
}
Android:
using System;
using System.ComponentModel;
using Android.Support.V4.Widget;
using Android.Views;
using Android.Widget;
using Mobile.XamarinForms.Droid.Controls.SideDrawer;
using Mobile.XamarinForms.Framework.Controls;
using Xamarin.Forms;
using Xamarin.Forms.Platform.Android;
using Android.Graphics;
using Application = Android.App.Application;
using Color = Android.Graphics.Color;
using RelativeLayout = Android.Widget.RelativeLayout;
using View = Android.Views.View;
[assembly: ExportRenderer(typeof(SideDrawer), typeof(SideDrawerRenderer))]
namespace Mobile.XamarinForms.Droid.Controls.SideDrawer
{
// public class SideDrawerRenderer : Xamarin.Forms.Platform.Android.AppCompat.ViewRenderer<Framework.Controls.SideDrawer, DrawerLayout>, DrawerLayout.IDrawerListener
public class SideDrawerRenderer : ViewRenderer<Framework.Controls.SideDrawer, DrawerLayout>, DrawerLayout.IDrawerListener
{
private DrawerLayout _nativeDrawerLayout;
private MarginLayoutParams _contentLayoutParameters;
private RelativeLayout _contentView;
private TextView _drawerView;
protected override void OnElementChanged(ElementChangedEventArgs<Framework.Controls.SideDrawer> e)
{
base.OnElementChanged(e);
if (this.Control == null)
{
InitializeNativeControl();
}
if (e.OldElement != null)
{
_nativeDrawerLayout.SetDrawerListener(null);
}
if (e.NewElement != null)
{
_nativeDrawerLayout.SetDrawerListener(this);
}
}
private void InitializeNativeControl()
{
_nativeDrawerLayout = new DrawerLayout(Context.ApplicationContext);
_nativeDrawerLayout.SetBackgroundColor(Element.BackgroundColor.ToAndroid());
_contentLayoutParameters = new MarginLayoutParams(LayoutParams.MatchParent, LayoutParams.MatchParent);
var layoutParamsDrawer = new DrawerLayout.LayoutParams(Element.DrawerLength, LinearLayout.LayoutParams.MatchParent);
layoutParamsDrawer.Gravity = GetDrawerGravity();
_drawerView = GetDrawerView(layoutParamsDrawer);
_contentView = GetContentView(_contentLayoutParameters);
// this one works, but i need the content from my forms property
var contentChild = new RelativeLayout(Context.ApplicationContext);
var contentChildLParams = new RelativeLayout.LayoutParams(300, 300);
contentChild.SetBackgroundColor(Color.Yellow);
_contentView.AddView(contentChild, contentChildLParams);
// i need to figure out how to make this work
// var contentRenderer = RendererFactory.GetRenderer(Element.MainContent);
// _contentView.AddView(contentRenderer.ViewGroup, new LayoutParams(LayoutParams.MatchParent, LayoutParams.MatchParent));
_nativeDrawerLayout.AddView(_drawerView);
_nativeDrawerLayout.AddView(_contentView);
SetNativeControl(_nativeDrawerLayout);
}
private int GetDrawerGravity()
{
switch (Element.DrawerPosition)
{
case DrawerPosition.Left:
return (int)GravityFlags.Start;
case DrawerPosition.Right:
return (int)GravityFlags.End;
default:
throw new ArgumentOutOfRangeException();
}
}
private RelativeLayout GetContentView(LayoutParams layoutParameters)
{
var view = new RelativeLayout(Context.ApplicationContext);
view.LayoutParameters = layoutParameters;
view.SetBackgroundColor(Color.Red);
return view;
}
private TextView GetDrawerView(LayoutParams layoutParameters)
{
var view = new TextView(Context.ApplicationContext);
view.LayoutParameters = layoutParameters;
view.SetBackgroundColor(Color.Purple);
view.SetTextColor(Color.Blue);
view.SetText("just some text", TextView.BufferType.Editable);
return view;
}
protected override void OnElementPropertyChanged(object sender, PropertyChangedEventArgs e)
{
base.OnElementPropertyChanged(sender, e);
switch (e.PropertyName)
{
case nameof(Framework.Controls.SideDrawer.Height):
break;
case nameof(Framework.Controls.SideDrawer.Width):
break;
}
}
public void OnDrawerClosed(View drawerView)
{
Element.IsOpen = false;
System.Diagnostics.Debug.WriteLine("OnDrawerClosed");
}
public void OnDrawerOpened(View drawerView)
{
Element.IsOpen = true;
System.Diagnostics.Debug.WriteLine("OnDrawerOpened");
}
public void OnDrawerSlide(View drawerView, float slideOffset)
{
switch (Element.DrawerPosition)
{
case DrawerPosition.Left:
_contentView.TranslationX = (int) Math.Abs(Element.DrawerLength*slideOffset);
break;
case DrawerPosition.Right:
_contentView.TranslationX = (int) Math.Abs(Element.DrawerLength*slideOffset)*-1;
break;
default:
throw new ArgumentOutOfRangeException();
}
_nativeDrawerLayout.BringChildToFront(_drawerView);
_nativeDrawerLayout.RequestLayout();
((ISideDrawerNativeEventProxy) Element)?.RaiseSlideChanged(slideOffset);
}
public void OnDrawerStateChanged(int newState)
{
// not really needed
// System.Diagnostics.Debug.WriteLine($"OnDrawerStateChanged {newState}");
}
}
}
How do i output the contents of e.g. MainContent (Shared project property)?
I was unable to find anything in xamarin docs about this and support is rather quiet about this topic so far (guess they are too busy).
Does anyone have experience with this issue?
Update: Reproduction solution
Android container:
internal sealed class FormsElementWrapper : FormsViewGroup
{
public override bool OnInterceptTouchEvent(MotionEvent ev)
{
return false;
}
private readonly IVisualElementRenderer _view;
public FormsElementWrapper(Xamarin.Forms.View content) : base(Application.Context)
{
_view = content != null ? Platform.CreateRenderer(content) : null;
if (_view == null)
return;
AddView(_view.ViewGroup);
}
protected override void OnLayout(bool changed, int left, int top, int right, int bottom)
{
if (_view == null)
return;
_view.Element.Layout(new Rectangle(0.0, 0.0, ContextExtensions.FromPixels(Context, right - left), ContextExtensions.FromPixels(Context, bottom - top)));
_view.UpdateLayout();
}
protected override void OnMeasure(int widthMeasureSpec, int heightMeasureSpec)
{
MeasureSpecMode widthMode = MeasureSpec.GetMode(widthMeasureSpec);
MeasureSpecMode heightMode = MeasureSpec.GetMode(heightMeasureSpec);
int widthSize = MeasureSpec.GetSize(widthMeasureSpec);
int heightSize = MeasureSpec.GetSize(heightMeasureSpec);
int pxHeight = (int) ContextExtensions.ToPixels(Context, _view.Element.HeightRequest);
int pxWidth = (int) ContextExtensions.ToPixels(Context, _view.Element.WidthRequest);
var measuredWidth = widthMode != MeasureSpecMode.Exactly ? (widthMode != MeasureSpecMode.AtMost ? pxHeight : Math.Min(pxHeight, widthSize)) : widthSize;
var measuredHeight = heightMode != MeasureSpecMode.Exactly ? (heightMode != MeasureSpecMode.AtMost ? pxWidth : Math.Min(pxWidth, heightSize)) : heightSize;
SetMeasuredDimension(measuredWidth, measuredHeight);
}
}
android drawer:
using System;
using System.ComponentModel;
using Android.Support.V4.Widget;
using Android.Views;
using Android.Widget;
using Xamarin.Forms;
using Xamarin.Forms.Platform.Android;
using Color = Android.Graphics.Color;
using RelativeLayout = Android.Widget.RelativeLayout;
using View = Android.Views.View;
[assembly: ExportRenderer(typeof(SideDrawer), typeof(SideDrawerRenderer))]
namespace MyNamespace.SideDrawer
{
public class SideDrawerRenderer : ViewRenderer<SideDrawer, DrawerLayout>, DrawerLayout.IDrawerListener
{
private DrawerLayout _nativeDrawerLayout;
private FormsElementWrapper _contentView;
private FormsElementWrapper _drawerView;
protected override void OnElementChanged(ElementChangedEventArgs<Framework.Controls.SideDrawer> e)
{
base.OnElementChanged(e);
if (this.Control == null)
{
InitializeNativeControl();
}
if (e.OldElement != null)
{
_nativeDrawerLayout.SetDrawerListener(null);
}
if (e.NewElement != null)
{
_nativeDrawerLayout.SetDrawerListener(this);
}
}
private void InitializeNativeControl()
{
_nativeDrawerLayout = new DrawerLayout(Context.ApplicationContext);
_nativeDrawerLayout.SetBackgroundColor(Element.BackgroundColor.ToAndroid());
AddDrawerLayer(_nativeDrawerLayout);
AddContentLayer(_nativeDrawerLayout);
SetNativeControl(_nativeDrawerLayout);
}
private void AddContentLayer(DrawerLayout nativeDrawerLayout)
{
_contentView = new FormsElementWrapper(Element.MainContent);
nativeDrawerLayout.AddView(_contentView);
}
private void AddDrawerLayer(DrawerLayout nativeDrawerLayout)
{
_drawerView = new FormsElementWrapper(Element.DrawerContent);
UpdateDrawerLength();
nativeDrawerLayout.AddView(_drawerView);
}
private int GetDrawerGravity()
{
switch (Element.DrawerPosition)
{
case DrawerPosition.Left:
return (int)GravityFlags.Start;
case DrawerPosition.Right:
return (int)GravityFlags.End;
default:
throw new ArgumentOutOfRangeException();
}
}
protected override void OnElementPropertyChanged(object sender, PropertyChangedEventArgs e)
{
base.OnElementPropertyChanged(sender, e);
switch (e.PropertyName)
{
case nameof(Framework.Controls.SideDrawer.Height):
break;
case nameof(Framework.Controls.SideDrawer.Width):
break;
case nameof(Framework.Controls.SideDrawer.MainContent):
_contentView = new FormsElementWrapper(Element.MainContent);
break;
case nameof(Framework.Controls.SideDrawer.DrawerContent):
_drawerView = new FormsElementWrapper(Element.DrawerContent);
break;
case nameof(Framework.Controls.SideDrawer.IsOpen):
UpdateDrawerStateProgramatically();
break;
case nameof(Framework.Controls.SideDrawer.DrawerLength):
case nameof(Framework.Controls.SideDrawer.DrawerPosition):
UpdateDrawerLength();
break;
}
}
private void UpdateDrawerLength()
{
var layoutParamsDrawer = new DrawerLayout.LayoutParams(Element.DrawerLength, ViewGroup.LayoutParams.MatchParent);
layoutParamsDrawer.Gravity = GetDrawerGravity();
_drawerView.LayoutParameters = layoutParamsDrawer;
}
private void UpdateDrawerStateProgramatically()
{
if (Element.IsOpen)
{
Control.OpenDrawer(GetDrawerGravity());
}
else
{
Control.CloseDrawer(GetDrawerGravity());
}
}
public void OnDrawerClosed(View drawerView)
{
Element.IsOpen = false;
System.Diagnostics.Debug.WriteLine("OnDrawerClosed");
}
public void OnDrawerOpened(View drawerView)
{
Element.IsOpen = true;
System.Diagnostics.Debug.WriteLine("OnDrawerOpened");
}
public void OnDrawerSlide(View drawerView, float slideOffset)
{
switch (Element.DrawerPosition)
{
case DrawerPosition.Left:
_contentView.TranslationX = (int)Math.Abs(Element.DrawerLength * slideOffset);
break;
case DrawerPosition.Right:
_contentView.TranslationX = (int)Math.Abs(Element.DrawerLength * slideOffset) * -1;
break;
default:
throw new ArgumentOutOfRangeException();
}
_nativeDrawerLayout.BringChildToFront(_drawerView);
_nativeDrawerLayout.RequestLayout();
((ISideDrawerNativeEventProxy)Element)?.RaiseSlideChanged(slideOffset);
}
public void OnDrawerStateChanged(int newState)
{
}
}
}
Xamarin.Forms Control:
public enum DrawerPosition
{
Left,
Right
}
public interface ISideDrawerNativeEventProxy
{
void RaiseSlideChanged(float percentage);
}
public class SideDrawer : Grid, ISideDrawerNativeEventProxy
{
#region IsOpen
public static BindableProperty IsOpenProperty = BindableProperty.Create<SideDrawer, bool>(d => d.IsOpen, default(bool), defaultBindingMode: BindingMode.TwoWay);
public bool IsOpen
{
get { return (bool) GetValue(IsOpenProperty); }
set { SetValue(IsOpenProperty, value); }
}
#endregion IsOpen
#region DrawerPosition
public static BindableProperty DrawerPositionProperty = BindableProperty.Create<SideDrawer, DrawerPosition>(d => d.DrawerPosition, default(DrawerPosition));
public DrawerPosition DrawerPosition
{
get { return (DrawerPosition) GetValue(DrawerPositionProperty); }
set { SetValue(DrawerPositionProperty, value); }
}
#endregion DrawerPosition
public static BindableProperty MainContentProperty = BindableProperty.Create<SideDrawer, View>(d => d.MainContent, default(View), propertyChanging: MainContentPropertyChanging, propertyChanged: MainContentPropertyChanged);
private static void MainContentPropertyChanged(BindableObject bindable, View oldValue, View newValue)
{
if (newValue == null)
return;
newValue.Parent = (View)bindable;
}
private static void MainContentPropertyChanging(BindableObject bindable, View oldValue, View newValue)
{
if (oldValue == null)
return;
oldValue.Parent = null;
}
public View MainContent
{
get { return (View) GetValue(MainContentProperty); }
set { SetValue(MainContentProperty, value); }
}
public static BindableProperty DrawerContentProperty = BindableProperty.Create<SideDrawer, View>(d => d.DrawerContent, default(View), propertyChanging: DrawerContentPropertyChanging, propertyChanged: DrawerContentPropertyChanged);
private static void DrawerContentPropertyChanged(BindableObject bindable, View oldValue, View newValue)
{
if (newValue == null)
return;
newValue.Parent = (View)bindable;
}
private static void DrawerContentPropertyChanging(BindableObject bindable, View oldValue, View newValue)
{
if (oldValue == null)
return;
oldValue.Parent = null;
}
public View DrawerContent
{
get { return (View) GetValue(DrawerContentProperty); }
set { SetValue(DrawerContentProperty, value); }
}
#region DrawerLength
public static BindableProperty DrawerLengthProperty = BindableProperty.Create<SideDrawer, int>(d => d.DrawerLength, default(int), defaultValueCreator: DrawerLengthDefault);
private static int DrawerLengthDefault(SideDrawer bindable)
{
return 300;
}
public int DrawerLength
{
get { return (int) GetValue(DrawerLengthProperty); }
set { SetValue(DrawerLengthProperty, value); }
}
#endregion DrawerLength
#region IsContentTranslated
public static BindableProperty IsContentTranslatedProperty = BindableProperty.Create<SideDrawer, bool>(d => d.IsContentTranslated, true);
public bool IsContentTranslated
{
get { return (bool) GetValue(IsContentTranslatedProperty); }
set { SetValue(IsContentTranslatedProperty, value); }
}
#endregion IsContentTranslated
void ISideDrawerNativeEventProxy.RaiseSlideChanged(float percentage)
{
}
protected override void OnBindingContextChanged()
{
base.OnBindingContextChanged();
if (MainContent != null)
SetInheritedBindingContext(MainContent, BindingContext);
if (DrawerContent != null)
SetInheritedBindingContext(DrawerContent, BindingContext);
}
}
The major flaws in my original implementation were:
lack of measurement data being forwarded
not forwarding parent hierarchy
not inheriting bindingcontext

xamarin design time binding cannot resolve property in data context

i'm currently in the process of modifying an ItemsView according to my needs. I noticed on flaw in my implementation however:
Unlike ListView i don't get intellisense according to my current iteration element. Does anyone know how to make that happen?
Here's my control implementation:
// http://adventuresinxamarinforms.com/2015/04/29/creating-a-xamarin-forms-accordion-control-without-custom-renders/
public class ItemsView : Grid
{
protected ScrollView ScrollView;
protected readonly StackLayout ItemsStackLayout;
public ItemsView()
{
ScrollView = new ScrollView();
ScrollView.SetBinding(ScrollOrientationProperty, new Binding(nameof(ScrollOrientation), mode: BindingMode.OneWay, source: this));
ItemsStackLayout = new StackLayout
{
Padding = new Thickness(0),
Spacing = 0,
HorizontalOptions = LayoutOptions.FillAndExpand
};
ItemsStackLayout.SetBinding(StackOrientationProperty, new Binding(nameof(ItemsStackLayout), mode: BindingMode.OneWay, source: this));
ScrollView.Content = ItemsStackLayout;
Children.Add(ScrollView);
SelectedCommand = new Command<object>(item =>
{
var selectable = item as ISelectable;
if (selectable == null)
return;
SetSelected(selectable);
SelectedItem = selectable.IsSelected ? selectable : null;
});
}
protected virtual void SetSelected(ISelectable selectable)
{
selectable.IsSelected = true;
}
public bool ScrollToStartOnSelected { get; set; }
#region SelectedCommand
public static BindableProperty SelectedCommandProperty = BindableProperty.Create<ItemsView, ICommand>(d => d.SelectedCommand, default(ICommand));
public ICommand SelectedCommand
{
get { return (ICommand) GetValue(SelectedCommandProperty); }
set { SetValue(SelectedCommandProperty, value); }
}
#endregion SelectedCommand
#region ScrollOrientation
public static BindableProperty ScrollOrientationProperty = BindableProperty.Create<ItemsView, ScrollOrientation>(d => d.ScrollOrientation, ScrollOrientation.Vertical);
public ScrollOrientation ScrollOrientation
{
get { return (ScrollOrientation) GetValue(ScrollOrientationProperty); }
set { SetValue(ScrollOrientationProperty, value); }
}
#endregion ScrollOrientation
#region StackOrientation
public static BindableProperty StackOrientationProperty = BindableProperty.Create<ItemsView, StackOrientation>(d => d.StackOrientation, StackOrientation.Vertical);
public StackOrientation StackOrientation
{
get { return (StackOrientation) GetValue(StackOrientationProperty); }
set { SetValue(StackOrientationProperty, value); }
}
#endregion StackOrientation
public event EventHandler SelectedItemChanged;
public static readonly BindableProperty ItemsSourceProperty =
BindableProperty.Create<ItemsView, IEnumerable>(p => p.ItemsSource, default(IEnumerable<object>), BindingMode.OneWay, null, ItemsSourceChanged);
public IEnumerable ItemsSource
{
get { return (IEnumerable)GetValue(ItemsSourceProperty); }
set { SetValue(ItemsSourceProperty, value); }
}
public static readonly BindableProperty SelectedItemProperty =
BindableProperty.Create<ItemsView, object>(p => p.SelectedItem, default(object), BindingMode.TwoWay, null, OnSelectedItemChanged);
public object SelectedItem
{
get { return (object)GetValue(SelectedItemProperty); }
set { SetValue(SelectedItemProperty, value); }
}
public static readonly BindableProperty ItemTemplateProperty =
BindableProperty.Create<ItemsView, DataTemplate>(p => p.ItemTemplate, default(DataTemplate));
public DataTemplate ItemTemplate
{
get { return (DataTemplate)GetValue(ItemTemplateProperty); }
set { SetValue(ItemTemplateProperty, value); }
}
private static void ItemsSourceChanged(BindableObject bindable, IEnumerable oldValue, IEnumerable newValue)
{
var itemsLayout = (ItemsView)bindable;
itemsLayout.SetItems();
var newObservableCasted = newValue as INotifyCollectionChanged;
var oldObservableCasted = oldValue as INotifyCollectionChanged;
if (newObservableCasted != null)
newObservableCasted.CollectionChanged += itemsLayout.ItemsSourceCollectionChanged;
if (oldObservableCasted != null)
oldObservableCasted.CollectionChanged -= itemsLayout.ItemsSourceCollectionChanged;
}
private void ItemsSourceCollectionChanged(object sender, NotifyCollectionChangedEventArgs args)
{
this.SetItems();
}
protected virtual void SetItems()
{
ItemsStackLayout.Children.Clear();
if (ItemsSource == null)
return;
foreach (var item in ItemsSource)
{
var itemView = GetItemView(item);
if (itemView == null)
{
ItemsStackLayout.Children.Add(new Label()
{
Text = "ItemTemplate missing."
});
break;
}
ItemsStackLayout.Children.Add(itemView);
}
SelectedItem = ItemsSource.OfType<ISelectable>().FirstOrDefault(x => x.IsSelected);
}
protected virtual View GetItemView(object item)
{
if (ItemTemplate == null)
return null;
ItemTemplate.SetValue(BindingContextProperty, item);
var content = ItemTemplate.CreateContent();
var view = content as View;
if (view == null)
return null;
var gesture = new TapGestureRecognizer
{
CommandParameter = item
};
gesture.SetBinding(TapGestureRecognizer.CommandProperty, (ItemsView v) => v.SelectedCommand, BindingMode.OneWay);
AddGesture(view, gesture);
return view;
}
protected 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 = (ItemsView)bindable;
if (newValue == oldValue)
return;
var selectable = newValue as ISelectable;
itemsView.SetSelectedItem(selectable ?? oldValue as ISelectable);
}
protected virtual void SetSelectedItem(ISelectable selectedItem)
{
var items = ItemsSource;
foreach (var item in items.OfType<ISelectable>())
item.IsSelected = selectedItem != null && item == selectedItem && selectedItem.IsSelected;
var handler = SelectedItemChanged;
if (handler != null)
handler(this, EventArgs.Empty);
}
}
my viewmodels:
public class InfoFieldsViewModel : HeaderedViewModel
{
public override Task NavigatedToAsync(object state)
{
base.NavigatedToAsync(state);
var casted = state as SiteSelectionEntry;
if (casted != null)
{
HeaderText = $" < {casted.Name}";
}
Groups.IsEventNotificationEnabled = false;
var group = new InfoFieldGroupViewModel("Gruppe 1");
group.Items.Add(new InfoFieldDetailViewModel("Label1"));
group.Items.Add(new InfoFieldDetailViewModel("Label2"));
group.Items.Add(new InfoFieldDetailViewModel("Label3"));
Groups.Add(group);
group = new InfoFieldGroupViewModel("Gruppe 2");
group.Items.Add(new InfoFieldDetailViewModel("Label4"));
group.Items.Add(new InfoFieldDetailViewModel("Label5"));
group.Items.Add(new InfoFieldDetailViewModel("Label6"));
Groups.Add(group);
Groups.IsEventNotificationEnabled = true;
Groups.RaiseUpdate();
return Done;
}
private ExtendedObservableCollection<InfoFieldGroupViewModel> _groups = new ExtendedObservableCollection<InfoFieldGroupViewModel>();
public ExtendedObservableCollection<InfoFieldGroupViewModel> Groups
{
get { return GetValue(ref _groups); }
set { SetValue(ref _groups, value); }
}
}
public class EditableViewModel : ViewModelBase
{
private bool _isInEditMode = new bool();
public bool IsInEditMode
{
get { return GetValue(ref _isInEditMode); }
set { SetValue(ref _isInEditMode, value); }
}
}
public class InfoFieldGroupViewModel : ViewModelBase
{
public InfoFieldGroupViewModel()
{
IsExpanded = true;
}
public InfoFieldGroupViewModel(string groupName)
{
_groupName = groupName;
}
private bool _isExpanded = new bool();
public bool IsExpanded
{
get { return GetValue(ref _isExpanded); }
set { SetValue(ref _isExpanded, value); }
}
private string _groupName;
public string GroupName
{
get { return _groupName; }
set { SetValue(ref _groupName, value); }
}
private ExtendedObservableCollection<InfoFieldDetailViewModel> _items = new ExtendedObservableCollection<InfoFieldDetailViewModel>();
public ExtendedObservableCollection<InfoFieldDetailViewModel> Items
{
get { return GetValue(ref _items); }
set { SetValue(ref _items, value); }
}
}
public class InfoFieldDetailViewModel : EditableViewModel
{
public InfoFieldDetailViewModel()
{
}
public InfoFieldDetailViewModel(string label)
{
_label = label;
}
private string _label;
public string Label
{
get { return _label; }
set { SetValue(ref _label, value); }
}
}
The view which uses the controls:
<Grid BackgroundColor="{x:Static resources:Colors.DefaultBackground}">
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
<RowDefinition Height="*" />
</Grid.RowDefinitions>
<custom:ApplicationHeader
HeaderText="{Binding HeaderText}"
HeaderTapCommand="{Binding NavigatorBackCommand}"
HomeButtonCommand="{Binding NavigatorBackCommand}"/>
<Grid Row="1" custom:GridExtensions.IsBusy="{Binding IsBusy}">
<custom:ItemsView ItemsSource="{Binding Groups}">
<custom:ItemsView.ItemTemplate>
<DataTemplate>
<Label MinimumHeightRequest="30" Text="{Binding GroupName}"></Label>
</DataTemplate>
</custom:ItemsView.ItemTemplate>
</custom:ItemsView>
</Grid>
</Grid>
Screenshot of designtime error:
Oddly enough an ordinary xamarin.forms listview seems to have no trouble getting the design time correct here and mapping the child datacontext within the item template.
Is there some attribute i'm missing out on to make it work? Or am i doing something wrong in my implementation which makes this fail? The template itself renders just fine. So it's just the design time getting it wrong here.
Any ideas welcome. So far none of my binding context redirects worked successfully.
For me, it was a design-time DataType specification added on page level:
<ContentPage xmlns="http://xamarin.com/schemas/2014/forms"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
xmlns:viewModels="clr-namespace:**;assembly=**"
xmlns:bindingConverters="clr-namespace:**;assembly=**"
x:DataType="viewModels:WelcomeViewModel" <!-- HERE-->
x:Class="**.WelcomePage"
Title="{Binding Title}">
<ContentPage.Content >
<CollectionView ItemsSource="{Binding Items}">
<CollectionView.ItemTemplate>
<DataTemplate>
<Label Grid.Row="0" Grid.Column="0" Text="{Binding Name}"/> <!-- this binding was looking for property 'Name' on root level, which is 'WelcomeViewModel' -->
</DataTemplate>
</CollectionView.ItemTemplate>
</CollectionView>
</ContentPage.Content>
</ContentPage>
So, I've just removed x:DataType="viewModels:WelcomeViewModel" and it started working.

Resources