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
}
}
Related
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();
}
}
If I have an Entry in my Xamarin Forms project and I keep typing on it until it goes off the end of the viewable page the text does not horizontally scroll in iOS as it does on Android - is there a way to make it do this?
I've tried replacing it with an Editor, but that doesn't have a placeholder, which I need.
An Entry is rendered as a UITextField on iOS which does not support scrolling. An Editor is rendered as a UITextView which does support scrolling.
One option would be to create a custom editor that includes a placeholder.
I have code for this. It's not pretty but it works.
Here is the CustomEditor code including new bindable properties for placeholder text and color.
public class CustomEditor : Editor
{
public static readonly BindableProperty PlaceholderProperty =
BindableProperty.Create(nameof(Placeholder), typeof(string), typeof(CustomEditor), default(string));
public string Placeholder
{
get { return (string)GetValue(PlaceholderProperty); }
set { SetValue(PlaceholderProperty, value); }
}
public static readonly BindableProperty PlaceholderColorProperty =
BindableProperty.Create(nameof(PlaceholderColor), typeof(Color), typeof(CustomEditor), default(Color));
public Color PlaceholderColor
{
get { return (Color)GetValue(PlaceholderColorProperty); }
set { SetValue(PlaceholderColorProperty, value); }
}
public static readonly BindableProperty DisabledColorProperty =
BindableProperty.Create(nameof(DisabledColor), typeof(Color), typeof(CustomEditor), default(Color));
public Color DisabledColor
{
get { return (Color)GetValue(DisabledColorProperty); }
set { SetValue(DisabledColorProperty, value); }
}
}
Here is the iOS custom renderer
[assembly: ExportRenderer(typeof(CustomEditor), typeof(CustomEditorRenderer))]
namespace Test.iOS.CustomRenderers
{
public class CustomEditorRenderer : EditorRenderer
{
private UILabel PlaceholderLabel { get; set; }
protected override void OnElementChanged(ElementChangedEventArgs<Editor> e)
{
base.OnElementChanged(e);
if (Control == null) return;
Control.Layer.BorderWidth = 0.5f;
Control.Layer.CornerRadius = 8;
Control.Layer.BorderColor = Color.FromHex("CDCDCD").ToCGColor();
if (PlaceholderLabel != null) return;
var element = Element as CustomEditor;
PlaceholderLabel = new UILabel
{
Text = element?.Placeholder,
TextColor = element?.PlaceholderColor.ToUIColor(),
BackgroundColor = UIColor.Clear
};
var edgeInsets = Control.TextContainerInset;
var lineFragmentPadding = Control.TextContainer.LineFragmentPadding;
Control.AddSubview(PlaceholderLabel);
var vConstraints = NSLayoutConstraint.FromVisualFormat(
"V:|-" + edgeInsets.Top + "-[PlaceholderLabel]-" + edgeInsets.Bottom + "-|", 0, new NSDictionary(),
NSDictionary.FromObjectsAndKeys(
new NSObject[] { PlaceholderLabel }, new NSObject[] { new NSString("PlaceholderLabel") })
);
var hConstraints = NSLayoutConstraint.FromVisualFormat(
"H:|-" + lineFragmentPadding + "-[PlaceholderLabel]-" + lineFragmentPadding + "-|",
0, new NSDictionary(),
NSDictionary.FromObjectsAndKeys(
new NSObject[] { PlaceholderLabel }, new NSObject[] { new NSString("PlaceholderLabel") })
);
PlaceholderLabel.TranslatesAutoresizingMaskIntoConstraints = false;
Control.AddConstraints(hConstraints);
Control.AddConstraints(vConstraints);
PlaceholderLabel.Hidden = Element.Text.NotEmpty();
}
protected override void OnElementPropertyChanged(object sender, System.ComponentModel.PropertyChangedEventArgs e)
{
base.OnElementPropertyChanged(sender, e);
if (e.PropertyName == "Text")
{
PlaceholderLabel.Hidden = Control.Text.NotEmpty();
}
if (e.PropertyName == VisualElement.IsEnabledProperty.PropertyName)
{
var element = Element as CustomEditor;
SetTextColor(element);
}
}
private void SetTextColor(CustomEditor element)
{
Control.TextColor = element.IsEnabled ? element.TextColor.ToUIColor() : element.DisabledColor.ToUIColor();
}
}
}
The following example works fine (https://developer.xamarin.com/samples/xamarin-forms/UserInterface/BindablePicker/)
When i try to implement it in my code, the object referenced for selectedItem is not being set. The picker is loading and selecting data fine, just not updating the object.
Here is some of the code i'm using:
XAML Page
<Picker x:Name="testpicker" Title="Select a Service" ItemsSource="{Binding Services}, Mode=TwoWay}" ItemDisplayBinding="{Binding ServiceDescription}" SelectedItem="{Binding SelectedServiceName, Mode=TwoWay}" />
I have the object in the view model, but this is never called when the picker items are selected.:
string selectedServiceName;
public string SelectedServiceName
{
get { return selectedServiceName; }
set
{
if (selectedServiceName != value)
{
selectedServiceName = value;
PickerOnPropertyChanged();
PickerOnPropertyChanged("SelectedService");
}
}
}
The binding is done from the controller when the view loads by the way....
protected async override void OnAppearing()
{
base.OnAppearing();
await viewModel.LoadPreferenceData();
await viewModel.LoadServiceData();
testpicker.SelectedIndex = 5;
}
I've also updated the base class to reflect the tutorial, i've changed the names.
Can you see anything obvious why this is not working? I'm happy to supply more code if needed.
The error was due to binding the picker to a custom type for the source.
ItemsSource="{Binding Services}
Instead of using a string for the binding object, i changed the type from:
public String SelectedServiceName
To this:
public Service SelectedServiceName
Create custom picker and implement in your code its working for me try below code :
public class CustomPicker : Picker
{
public CustomPicker()
{
SelectedIndexChanged += OnSelectedIndexChanged;
}
public static readonly BindableProperty SelectedItemProperty =
BindableProperty.Create("SelectedItem", typeof(object), typeof(CustomPicker), null, BindingMode.TwoWay, null, OnSelectedItemChanged);
public object SelectedItem
{
get { return GetValue(SelectedItemProperty); }
set
{
SetValue(SelectedItemProperty, value);
if (value != null && ItemsSource!=null && ItemsSource.Contains(value))
SelectedIndex = ItemsSource.IndexOf(value);
else
SelectedIndex = -1;
}
}
public static readonly BindableProperty ItemsSourceProperty =
BindableProperty.Create("ItemsSource", typeof(IEnumerable), typeof(CustomPicker), null, BindingMode.TwoWay, null, OnItemsSourceChanged);
public IList ItemsSource
{
get { return (IList)GetValue(ItemsSourceProperty); }
set { SetValue(ItemsSourceProperty, value); }
}
public static readonly BindableProperty DisplayPropertyProperty =
BindableProperty.Create("DisplayProperty", typeof(string), typeof(CustomPicker), null, BindingMode.TwoWay, null, OnDisplayPropertyChanged);
public string DisplayProperty
{
get { return (string)GetValue(DisplayPropertyProperty); }
set { SetValue(DisplayPropertyProperty, value); }
}
private static void OnSelectedItemChanged(BindableObject bindable, object oldValue, object newValue)
{
var picker = (CustomPicker)bindable;
picker.SelectedItem = newValue;
if (picker.ItemsSource != null && picker.SelectedItem != null)
{
var count = 0;
foreach (var obj in picker.ItemsSource)
{
if (obj == picker.SelectedItem)
{
picker.SelectedIndex = count;
break;
}
count++;
}
}
}
private static void OnDisplayPropertyChanged(BindableObject bindable, object oldValue, object newValue)
{
var picker = (CustomPicker)bindable;
picker.DisplayProperty = (string)newValue;
LoadItemsAndSetSelected(bindable);
}
private static void OnItemsSourceChanged(BindableObject bindable, object oldValue, object newValue)
{
var picker = (CustomPicker)bindable;
picker.ItemsSource = (IList)newValue;
var oc = newValue as INotifyCollectionChanged;
if (oc != null)
{
oc.CollectionChanged += (a, b) =>
{
LoadItemsAndSetSelected(bindable);
};
}
LoadItemsAndSetSelected(bindable);
}
private static void LoadItemsAndSetSelected(BindableObject bindable)
{
var picker = (CustomPicker)bindable;
if (picker.ItemsSource == null)
return;
var count = 0;
foreach (var obj in picker.ItemsSource)
{
var value = string.Empty;
if (picker.DisplayProperty != null)
{
var prop = obj.GetType().GetRuntimeProperties().FirstOrDefault(p => string.Equals(p.Name, picker.DisplayProperty, StringComparison.OrdinalIgnoreCase));
if (prop != null)
value = prop.GetValue(obj).ToString();
}
else
{
value = obj.ToString();
}
if (!picker.Items.Contains(value))
{
picker.Items.Add(value);
}
if (picker.SelectedItem != null && picker.SelectedItem == obj)
picker.SelectedIndex = count;
count++;
}
if (picker.ItemsSource.Count == picker.Items.Count - 1)
picker.SelectedIndex++;
}
private void OnSelectedIndexChanged(object sender, EventArgs e)
{
if (SelectedIndex > -1)
{
SelectedItem = ItemsSource[SelectedIndex];
}
}
}
Xaml Code
<userControls:CustomPicker BackgroundColor="Transparent" x:Name="testpicker" HorizontalOptions="FillAndExpand" ItemsSource="{Binding Services}" SelectedItem="{Binding SelectedServiceName}" DisplayProperty="{Binding ServiceDescription}" />
Don't forgot put in Xaml header
xmlns:userControls="clr-namespace:MyNameSpace"
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.
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.