I have an application where I can change the order and the way cards appear. For anyone who has iOS I need something very similar to the way the Settings > Contacts > Sort Order page works.
This shows two rows. One with First, Last and the other with Last, First. When a user clicks on a row it acts like a radio button and a tick mark appears at the end of the row.
I would like to try and implement this functionality but I am not sure where to start. Should I do this with a ViewCell or a TextCell and how does anyone have any ideas as to how it is implemented this
.
EDIT 1: Simplified property changed logic in iOS renderer; now there are no references or handlers to cleanup.
In extension to #hankide's answer:
You can create a bindable property IsChecked while extending a TextCell or ViewCell and bind your VM state to it.
public class MyTextCell : TextCell
{
public static readonly BindableProperty IsCheckedProperty =
BindableProperty.Create(
"IsChecked", typeof(bool), typeof(MyTextCell),
defaultValue: false);
public bool IsChecked
{
get { return (bool)GetValue(IsCheckedProperty); }
set { SetValue(IsCheckedProperty, value); }
}
}
Next step would be to create renderer that listens to this property and shows a check-mark at iOS level.
[assembly: ExportRenderer(typeof(MyTextCell), typeof(SampleApp.iOS.MyTextCellRenderer))]
namespace SampleApp.iOS
{
public class MyTextCellRenderer : TextCellRenderer
{
public override UITableViewCell GetCell(Cell item, UITableViewCell reusableCell, UITableView tv)
{
var nativeCell = base.GetCell(item, reusableCell, tv);
var formsCell = item as MyTextCell;
SetCheckmark(nativeCell, formsCell);
return nativeCell;
}
protected override void HandlePropertyChanged(object sender, PropertyChangedEventArgs args)
{
base.HandlePropertyChanged(sender, args);
System.Diagnostics.Debug.WriteLine($"HandlePropertyChanged {args.PropertyName}");
if (args.PropertyName == MyTextCell.IsCheckedProperty.PropertyName)
{
var nativeCell = sender as CellTableViewCell;
if (nativeCell?.Element is MyTextCell formsCell)
SetCheckmark(nativeCell, formsCell);
}
}
void SetCheckmark(UITableViewCell nativeCell, MyTextCell formsCell)
{
if (formsCell.IsChecked)
nativeCell.Accessory = UITableViewCellAccessory.Checkmark;
else
nativeCell.Accessory = UITableViewCellAccessory.None;
}
}
}
Sample usage 1
And, sample usage would like:
<TableView Intent="Settings">
<TableSection Title="Sort Order">
<local:MyTextCell Text="First Last" IsChecked="false" />
<local:MyTextCell Text="Last, First" IsChecked="true" />
</TableSection>
</TableView>
Sample usage 2
You can also listen to Tapped event to ensure IsChecked property works as expected.
For example, you bind this property to ViewModel:
<TableView Intent="Settings">
<TableSection Title="Sort Order">
<local:MyTextCell Tapped="Handle_Tapped" Text="{Binding [0].Name}" IsChecked="{Binding [0].IsSelected}" />
<local:MyTextCell Tapped="Handle_Tapped" Text="{Binding [1].Name}" IsChecked="{Binding [1].IsSelected}" />
</TableSection>
</TableView>
and handle tap event:
public SettingViewModel[] Settings = new []{
new SettingViewModel { Name = "First Last", IsSelected = false },
new SettingViewModel { Name = "Last First", IsSelected = true },
};
void Handle_Tapped(object sender, System.EventArgs e)
{
var cell = sender as TextCell;
if (cell == null)
return;
var selected = cell.Text;
foreach(var setting in Settings)
{
if (setting.Name == selected)
setting.IsSelected = true;
else
setting.IsSelected = false;
}
}
The sort order settings page you described is implemented using the UIKit's UITableView. In Xamarin.Forms, you can utilize the TableView control to get the same result.
As you will quickly notice, there's no way to set the checkmark icon with Xamarin.Forms so you'll probably need to create a custom cell, that has the text on the left and the checkmark image on the right.
If you really want to do everything by the book, you should probably create a custom renderer that allows you to set the Accessory property of the current ViewCell. However, this will get a bit complex for such a small feature.
Related
Xamarin Form View Model can trigger the onTextChange Event for Searchbar but there is no Event handler for OnCancelButtonClicked.
What I want:
An Event should be Triggered whenever Cancel/Close Button is clicked as below.
You can get Searchbar CloseButton event in SearchBar custom render, but I think it is not useful for your goal.
I suggest you can click search icon to refresh data source. I don one sample using Searchbar and ListView, you can take a look:
[assembly: ExportRenderer(typeof(SearchBar), typeof(CustomSearchBarRenderer))]
namespace FormsSample.Droid
{
public class CustomSearchBarRenderer: SearchBarRenderer
{
public CustomSearchBarRenderer(Context context):base(context)
{
}
protected override void OnElementChanged(ElementChangedEventArgs<SearchBar> e)
{
base.OnElementChanged(e);
if (Control != null)
{
var searchView = Control;
searchView.Iconified = true;
searchView.SetIconifiedByDefault(false);
int searchCloseButtonId = Context.Resources.GetIdentifier("android:id/search_close_btn", null, null);
// search close button icon, you can add event for closeIcon.click.
var closeIcon = searchView.FindViewById(searchCloseButtonId);
int searchViewSearchButtonId = Control.Resources.GetIdentifier("android:id/search_mag_icon", null, null);
var searchIcon = searchView.FindViewById(searchViewSearchButtonId);
searchIcon.Click += SearchIcon_Click;
}
}
private void SearchIcon_Click(object sender, EventArgs e)
{
Element.OnSearchButtonPressed();
}
}
}
<StackLayout>
<SearchBar
x:Name="searchBar"
HorizontalOptions="Fill"
Placeholder="Search fruits..."
SearchButtonPressed="OnSearchButtonPressed"
VerticalOptions="CenterAndExpand" />
<Label
HorizontalOptions="Fill"
Text="Enter a search term and press enter or click the magnifying glass to perform a search."
VerticalOptions="CenterAndExpand" />
<ListView
x:Name="searchResults"
HorizontalOptions="Fill"
VerticalOptions="CenterAndExpand" />
</StackLayout>
public partial class Page23 : ContentPage
{
public Page23()
{
InitializeComponent();
searchResults.ItemsSource = DataService.Fruits;
}
private void OnSearchButtonPressed(object sender, EventArgs e)
{
if(string.IsNullOrEmpty(searchBar.Text))
{
searchResults.ItemsSource = DataService.Fruits;
}
else
{
searchResults.ItemsSource = DataService.GetSearchResults(searchBar.Text);
}
}
}
You can click search closebutton to move text in SearchBar firstly, then click SearchButton to refresh data for listView.
I needed to use a Treeview in my xamarin forms application, however the only existing TreeView on the net are not free (Syncfusion and Telerik).
So I found this very interesting project : https://github.com/AdaptSolutions/Xamarin.Forms-TreeView
the only problem that I found is that the ItemSource and SelectedItem properties are not bindable and therefor I can't use it on an MVVM Pattern. Which brings us to my question, How can I make them bindable.
I tried to follow this documentation : https://learn.microsoft.com/en-us/xamarin/xamarin-forms/xaml/bindable-properties
but still nothing. Can anyone help me with that please ? Thank you
UPDATE :
this is the TreeView class :
public class TreeView : ScrollView
{
#region Fields
private readonly StackLayout _StackLayout = new StackLayout { Orientation = StackOrientation.Vertical };
//TODO: This initialises the list, but there is nothing listening to INotifyCollectionChanged so no nodes will get rendered
private IList<TreeViewNode> _RootNodes = new ObservableCollection<TreeViewNode>();
private TreeViewNode _SelectedItem;
#endregion
#region Public Properties
public TreeViewNode SelectedItem
{
get => _SelectedItem;
set
{
if (_SelectedItem == value)
{
return;
}
if (_SelectedItem != null)
{
_SelectedItem.IsSelected = false;
}
_SelectedItem = value;
SelectedItemChanged?.Invoke(this, new EventArgs());
}
}
public IList<TreeViewNode> RootNodes
{
get => _RootNodes;
set
{
_RootNodes = value;
if (value is INotifyCollectionChanged notifyCollectionChanged)
{
notifyCollectionChanged.CollectionChanged += (s, e) =>
{
RenderNodes(_RootNodes, _StackLayout, e, null);
};
}
RenderNodes(_RootNodes, _StackLayout, new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Reset), null);
}
}
#endregion
#region Constructor
public TreeView()
{
Content = _StackLayout;
}
#endregion
#region Private Static Methods
private static void AddItems(IEnumerable<TreeViewNode> childTreeViewItems, StackLayout parent, TreeViewNode parentTreeViewItem)
{
foreach (var childTreeNode in childTreeViewItems)
{
if (!parent.Children.Contains(childTreeNode))
{
parent.Children.Add(childTreeNode);
}
childTreeNode.ParentTreeViewItem = parentTreeViewItem;
}
}
#endregion
#region Internal Static Methods
internal static void RenderNodes(IEnumerable<TreeViewNode> childTreeViewItems, StackLayout parent, NotifyCollectionChangedEventArgs e, TreeViewNode parentTreeViewItem)
{
if (e.Action != NotifyCollectionChangedAction.Add)
{
AddItems(childTreeViewItems, parent, parentTreeViewItem);
}
else
{
AddItems(e.NewItems.Cast<TreeViewNode>(), parent, parentTreeViewItem);
}
}
#endregion
}
so what Im trying to do here is making RootNodes bindable as well as SelectedItem afterwards.
What I did is simply adding this, thinking it should work but obviously it does not :
public static readonly BindableProperty RootNodesProperty =
BindableProperty.Create(nameof(RootNodes), typeof(IList<TreeViewNode>), typeof(TreeView));
public IList<TreeViewNode> RootNodes
{
get => (IList<TreeViewNode>)GetValue(RootNodesProperty);
set
{
SetValue(RootNodesProperty, value);
_RootNodes = value;
if (value is INotifyCollectionChanged notifyCollectionChanged)
{
notifyCollectionChanged.CollectionChanged += (s, e) =>
{
RenderNodes(_RootNodes, _StackLayout, e, null);
};
}
RenderNodes(_RootNodes, _StackLayout, new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Reset), null);
}
}
UPDATE 2 :
Here is what it looks like
Hope this helps
It seems you will not need to create custom ItemSource and SelectedItem in ScrollView, because Xamarin Foms has Bindable Layouts that contains ItemsSource and ItemTemplateSelector .
Bindable layouts enable any layout class that derives from the Layout class to generate its content by binding to a collection of items, with the option to set the appearance of each item with a DataTemplate. Bindable layouts are provided by the BindableLayout class, which exposes the following attached properties:
ItemsSource – specifies the collection of IEnumerable items to be displayed by the layout.
ItemTemplate – specifies the DataTemplate to apply to each item in the collection of items displayed by the layout.
ItemTemplateSelector – specifies the DataTemplateSelector that will be used to choose a DataTemplate for an item at runtime.
If you need to use ScrollView, sample code as follows:
<ScrollView>
<StackLayout BindableLayout.ItemsSource="{Binding User.TopFollowers}"
Orientation="Horizontal"
...>
<BindableLayout.ItemTemplate>
<DataTemplate>
<controls:CircleImage Source="{Binding}"
Aspect="AspectFill"
WidthRequest="44"
HeightRequest="44"
... />
</DataTemplate>
</BindableLayout.ItemTemplate>
</StackLayout>
</ScrollView>
I'm trying to use a custom ViewCellRenderer for iOS in Xamarin Forms, but the renderer never called. I'm using a custom ListViewRenderer too and that is working like a charm. Anyone have any idea about this?
Here is my TableViewRenderer
[assembly: ExportRenderer(typeof(ListView), typeof(CustomTableViewRenderer))]
namespace OdontoWayPaciente.Mobile.iOS.Renderers
{
public class CustomTableViewRenderer : ListViewRenderer
{
protected override void OnElementChanged(ElementChangedEventArgs<ListView> e)
{
base.OnElementChanged(e);
if (Control != null && e.NewElement != null)
{
Control.TableFooterView = new UIView(CGRect.Empty);
if (e.NewElement.IsGroupingEnabled)
{
var groupedTableView = new UITableView(Control.Frame, UITableViewStyle.Grouped);
groupedTableView.Source = Control.Source;
SetNativeControl(groupedTableView);
}
}
}
}
}
Here is my ViewCellRenderer:
[assembly: ExportRenderer(typeof(ViewCell), typeof(CustomAllViewCellRendereriOS))]
namespace OdontoWayPaciente.Mobile.iOS.Renderers
{
public class CustomAllViewCellRendereriOS : ViewCellRenderer
{
public override UITableViewCell GetCell(Cell item, UITableViewCell reusableCell, UITableView tableView)
{
var cell = base.GetCell(item, reusableCell, tableView);
if (cell != null)
{
cell.SelectionStyle = UITableViewCellSelectionStyle.None;
}
if (tableView.Style == UITableViewStyle.Grouped)
{
cell.TintColor = UIColor.Blue;
cell.BackgroundColor = UIColor.Blue;
}
return cell;
}
}
}
Here is my ViewCell XAML code:
<ViewCell Height="{StaticResource listItem_alturaLinhaUnica}"
StyleId="disclosure">
<ContentView>
<ContentView.BackgroundColor>
<OnPlatform x:TypeArguments="Color" iOS="White"/>
</ContentView.BackgroundColor>
<AbsoluteLayout VerticalOptions="Center" BackgroundColor="White">
<Label
Text="{Binding TextoAcesso}"
Style="{DynamicResource TextoPrimario}">
<Label.Margin>
<OnPlatform x:TypeArguments="Thickness">
<On Platform="iOS" Value="18, 8, 0, 8"/>
</OnPlatform>
</Label.Margin>
</Label>
</AbsoluteLayout>
</ContentView>
</ViewCell>
Please check your ListView's ItemsSource, the ListView should have at least one cell then the CustomViewRenderer will be called.
if I use ListView with GroupingEnabled=True, CustomViewRenderer is not
called, but GroupingEnabled=false, CustomViewCellRenderer is called
Whether you just add some groups in your ItemsSource like: new List<Group> { group1, group2 };, but each group contains none item. When you set the IsGroupingEnabled to false, the ListView has two cells in this case so renderer called. But if the IsGroupingEnabled is true, the ListView just has two sections without any cells then renderer will not be called.
I am using a renderer to allow me to set a custom footer in my TableView. The renderer works but I would like to have the capability to set up different footers for the different table sections. For example one footer for table section 0 and another for table section 1, all the way up to table section 5.
Here's the XAML that I am using:
<!-- <local:ExtFooterTableView x:Name="tableView" Intent="Settings" HasUnevenRows="True">-->
<TableView x:Name="tableView" Intent="Settings" HasUnevenRows="True">
<TableSection Title="Cards1">
<ViewCell Height="50">
<Label Text="Hello1" />
</ViewCell>
<ViewCell Height="50">
<Label Text="Hello2" />
</ViewCell>
</TableSection>
<TableSection Title="Cards2">
<TextCell Height="50" Text="Hello"></TextCell>
</TableSection>
</TableSection>
<!-- </local:ExtFooterTableView>-->
</TableView>
and here is the C# class and renderer:
public class ExtFooterTableView : TableView
{
public ExtFooterTableView()
{
}
}
and:
using System;
using Japanese;
using UIKit;
using Xamarin.Forms;
using Xamarin.Forms.Platform.iOS;
[assembly: ExportRenderer(typeof(ExtFooterTableView), typeof(Japanese.iOS.ExtFooterTableViewRenderer))]
namespace Japanese.iOS
{
public class ExtFooterTableViewRenderer : TableViewRenderer
{
protected override void OnElementChanged(ElementChangedEventArgs<TableView> e)
{
base.OnElementChanged(e);
if (Control == null)
return;
var tableView = Control as UITableView;
var formsTableView = Element as TableView;
tableView.WeakDelegate = new CustomFooterTableViewModelRenderer(formsTableView);
}
private class CustomFooterTableViewModelRenderer : TableViewModelRenderer
{
public CustomFooterTableViewModelRenderer(TableView model) : base(model)
{
}
public override UIView GetViewForFooter(UITableView tableView, nint section)
{
Debug.WriteLine("xx");
if (section == 0)
{
return new UILabel()
{
// Text = TitleForFooter(tableView, section), // or use some other text here
Text = "abc",
TextAlignment = UITextAlignment.Left
// TextAlignment = NSTextAlignment.NSTextAlignmentJustified
};
}
else
{
return new UILabel()
{
// Text = TitleForFooter(tableView, section), // or use some other text here
Text = "def",
TextAlignment = UITextAlignment.Left
// TextAlignment = NSTextAlignment.NSTextAlignmentJustified
};
}
}
}
}
}
The code works but I would like to find out how I can set up a different footer text for different sections in the XAML. Something like this:
From what I see it looks like the code is partly there TitleForFooter(tableView, section) but I am not sure how to use it and how I could set it up. Note that I am not really looking for a view model solution. I would be happy to be simply able to specify the section footer text as part of the TableView XAML.
I'd appreciate if anyone could give me some advice on this.
First of all, in order to be able to specify the section footer text in XAML - simplest option would be to create a bindable property in TableSection. But as TableSection is sealed, we can't derive it to define our custom bindable properties.
So, the next option is to create a attached bindable property.
public class Ex
{
public static readonly BindableProperty FooterTextProperty =
BindableProperty.CreateAttached("FooterText", typeof(string), typeof(Ex), defaultValue: default(string));
public static string GetFooterText(BindableObject view)
{
return (string)view.GetValue(FooterTextProperty);
}
public static void SetFooterText(BindableObject view, string value)
{
view.SetValue(FooterTextProperty, value);
}
}
Next step would be to update renderer to retrieve this value for every section:
private class CustomFooterTableViewModelRenderer : TableViewModelRenderer
{
public CustomFooterTableViewModelRenderer(TableView model) : base(model)
{
}
public override UIView GetViewForFooter(UITableView tableView, nint section)
{
return new UILabel()
{
Text = TitleForFooter(tableView, section), // or use some other text here
Font = UIFont.SystemFontOfSize(14),
ShadowColor = Color.White.ToUIColor(),
ShadowOffset = new CoreGraphics.CGSize(0, 1),
TextColor = Color.DarkGray.ToUIColor(),
BackgroundColor = Color.Transparent.ToUIColor(),
Opaque = false,
TextAlignment = UITextAlignment.Center
};
}
//Retrieves the footer text for corresponding section through the attached property
public override string TitleForFooter(UITableView tableView, nint section)
{
var tblSection = View.Root[(int)section];
return Ex.GetFooterText(tblSection);
}
}
Sample Usage
<local:ExtFooterTableView x:Name="tableView" Intent="Settings" HasUnevenRows="True">
<TableSection Title="Cards1" local:Ex.FooterText="Sample description">
<ViewCell Height="50">
<Label Margin="20,0,20,0" Text="Hello1" />
</ViewCell>
<ViewCell Height="50">
<Label Margin="20,0,20,0" Text="Hello2" />
</ViewCell>
</TableSection>
<TableSection Title="Cards2" local:Ex.FooterText="Disclaimer note">
<TextCell Height="50" Text="Hello"></TextCell>
</TableSection>
</local:ExtFooterTableView>
It is very simple. you need to add the bindable property for pass value from XAML to CustomRenderer in CustomControl like this:
Customer TableView
public class ExtFooterTableView : TableView
{
public ExtFooterTableView()
{
}
}
Xaml control code
<local:ExtFooterTableView x:Name="tableView" Intent="Settings" HasUnevenRows="True">
Renderer class
using System;
using UIKit;
using Xamarin.Forms;
using Xamarin.Forms.Platform.iOS;
using yournamespace;
using System.ComponentModel;
[assembly: ExportRenderer(typeof(ExtFooterTableView), typeof(FooterTableViewRenderer))]
namespace yournamespace
{
public class FooterTableViewRenderer : TableViewRenderer
{
protected override void OnElementChanged(ElementChangedEventArgs<TableView> e)
{
base.OnElementChanged(e);
}
protected override void OnElementPropertyChanged(object sender, PropertyChangedEventArgs e)
{
base.OnElementPropertyChanged(sender, e);
var view = (ExtFooterTableView)Element;
if (e.PropertyName == ExtFooterTableView.IntentProperty.PropertyName)
{
string intent = view.Intent;
// Do your stuff for intent property
}
if (e.PropertyName == ExtFooterTableView.HasUnevenRowsProperty.PropertyName)
{
bool hasUnevenRows = view.HasUnevenRows;
// Do yout stuff for HasUnevenRow
}
}
}
}
In this case isn't supposed that I can see the values from list in ListPicker?
xaml
<toolkit:ListPicker
x:Name="lpkBoards"
ItemsSource="{Binding AllBoards}"
DisplayMemberPath="Name" >
</toolkit:ListPicker>
xaml.cs
public SettingsPage()
{
InitializeComponent();
// Set the page DataContext property to the ViewModel.
this.DataContext = App.ViewModel;
...
boardsTask.ContinueWith(
(call) =>
{
App.ViewModel.AllBoards = call.Result.ToList();
}
);
ViewModel
// All to-do items.
private List<Board> _allBoards;
public List<Board> AllBoards
{
get { return _allBoards; }
set
{
_allBoards = value;
NotifyPropertyChanged("AllBoards");
}
}
You need to change the List<Board> to ObservalbeCollection<Board> if you are trying to bind it to a UI element and want it to work.