Avalonia Dynamic Tree View MVVM - treeview

In Avalonia,
I want to create a file tree view.
When I write
<TreeView Height="500" Background="Red" >
<TreeViewItem Header="h0">
<TreeViewItem Header="h11"></TreeViewItem>
</TreeViewItem>
<TreeViewItem Header="h1"></TreeViewItem>
</TreeView>
It works, but obvisously, it's static.
I tried
<TreeView Height="500" Background="Red" Items="{Binding Files}">
<TreeView.ItemTemplate>
<TreeDataTemplate ItemsSource="{Binding Items}">
<TextBlock Text="{Binding Header}"/>
</TreeDataTemplate>
</TreeView.ItemTemplate>
</TreeView>
with the ViewModel:
public class RepoFileViewModel : ReactiveObject, IRoutableViewModel
{
public string? UrlPathSegment { get; } = Guid.NewGuid().ToString();
public IScreen HostScreen { get; }
public RepoFileViewModel(IScreen screen,)
{
HostScreen = screen;
TreeViewItem Root = new TreeViewItem();
TreeViewItem l1a = new TreeViewItem { Header = "level 1 a" };
TreeViewItem l1b = new TreeViewItem { Header = "level 1 b" };
TreeViewItem l1c = new TreeViewItem { Header = "level 1 c" };
Root.Items = new List<TreeViewItem>
{
{l1a },{l1b},{l1c }
};
TreeViewItem l2ba = new TreeViewItem { Name = "level 2 b a" };
l1b.Items = new List<TreeViewItem>
{
{ l2ba }
};
Files = Root;
this.RaisePropertyChanged("Files");
}
private TreeViewItem files;
public TreeViewItem Files
{
get => files;
set
{
this.RaiseAndSetIfChanged(ref files, value);
}
}
}
It does not work.
How to properly create a dynamic TreeView using MVVM and XAML in Avalonia?

<TreeView Items="{Binding Items}" >
<TreeView.ItemTemplate>
<TreeDataTemplate ItemsSource="{Binding Items}">
<TextBlock Text="{Binding Name}"/>
</TreeDataTemplate>
</TreeView.ItemTemplate>
</TreeView>
public RepoFileViewModel(IScreen screen)
{
HostScreen = screen;
FileTreeElement root = new FileTreeElement("root");
FileTreeElement e0 = new FileTreeElement("level 0 a");
FileTreeElement e1 = new FileTreeElement("level 0 b");
FileTreeElement e2 = new FileTreeElement("level 0 c");
FileTreeElement e3 = new FileTreeElement("level 1 a");
e1.Items.Add(e3);
root.Items.Add(e0);
root.Items.Add(e1);
root.Items.Add(e2);
this.Items = root.Items;
this.RaisePropertyChanged("Files");
}
private ObservableCollection<FileTreeElement> items;
public ObservableCollection<FileTreeElement> Items
{
get => items;
set
{
this.RaiseAndSetIfChanged(ref items, value);
}
}
works!

Related

Xamarin Forms: How to Change the textcolor of Collectionview SelectedItem?

I have a CarouselPage having 5 children and every child has a horizontal collection view. When selecting an item in Collectionview or swiping the pages, I need to give a different text color and need to add an underline for the selected item. I have tried like below:
CarouselHomePage.cs
public partial class CarouselHomePage : CarouselPage
{
public List<Activity> activityList { get; set; }
public CarouselHomePage()
{
InitializeComponent();
activityList = new List<Activity>();
AddActivities();
MessagingCenter.Subscribe<App, string>((App)Xamarin.Forms.Application.Current, "child", (s, child) =>
{
CurrentPage = Children[Int32.Parse(child)];
});
}
private void AddActivities()
{
activityList.Add(new Activity() { Title = "PageNumber1" });
activityList.Add(new Activity() { Title = "PageNumber2" });
activityList.Add(new Activity() { Title = "PageNumber3" });
activityList.Add(new Activity() { Title = "PageNumber4" });
activityList.Add(new Activity() { Title = "PageNumber5" });
AddChild(activityList);
}
public void AddChild(List<Activity> activityList)
{
this.Children.Add(new PageNumber1(activityList));
this.Children.Add(new PageNumber2(activityList));
this.Children.Add(new PageNumber3(activityList));
this.Children.Add(new PageNumber4(activityList));
this.Children.Add(new PageNumber5(activityList));
}
}
Activity.cs
public class Activity
{
public string Title { get; set; }
public bool visibility { get; set; }
public bool Visibility
{
set
{
if (value != null)
{
visibility = value;
NotifyPropertyChanged();
}
}
get
{
return visibility;
}
}
private Color textColor;
public Color TextColor
{
set
{
if (value != null)
{
textColor = value;
NotifyPropertyChanged();
}
}
get
{
return textColor;
}
}
public event PropertyChangedEventHandler PropertyChanged;
private void NotifyPropertyChanged([CallerMemberName] String propertyName = "")
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
}
PageNumber1.xaml
<ContentPage.Content>
<StackLayout Orientation="Vertical">
<CollectionView
SelectionMode="Single"
x:Name="ActivityList"
Margin="5,10,5,10"
SelectionChanged="TagItemTapped"
ItemsLayout="HorizontalList">
<CollectionView.ItemTemplate>
<DataTemplate>
<StackLayout
Orientation="Vertical"
Margin="15">
<Label
TextColor="{Binding TextColor}"
HorizontalTextAlignment="Center"
VerticalTextAlignment="Center"
Text="{Binding Title}">
<Label.FontSize>
<OnIdiom x:TypeArguments="x:Double">
<OnIdiom.Phone>18</OnIdiom.Phone>
<OnIdiom.Tablet>27</OnIdiom.Tablet>
<OnIdiom.Desktop>18</OnIdiom.Desktop>
</OnIdiom>
</Label.FontSize>
</Label>
<BoxView
HeightRequest="2"
IsVisible="{Binding Visibility}"
BackgroundColor="{Binding TextColor}"
HorizontalOptions="CenterAndExpand"
VerticalOptions="Start"/>
</StackLayout>
</DataTemplate>
</CollectionView.ItemTemplate>
<CollectionView.HeightRequest>
<OnIdiom x:TypeArguments="x:Double">
<OnIdiom.Phone>30</OnIdiom.Phone>
<OnIdiom.Tablet>60</OnIdiom.Tablet>
<OnIdiom.Desktop>30</OnIdiom.Desktop>
</OnIdiom>
</CollectionView.HeightRequest>
</CollectionView>
<Label Text="Welcome to PageNumber1"
VerticalOptions="CenterAndExpand"
HorizontalOptions="CenterAndExpand" />
</StackLayout>
</ContentPage.Content>
PageNumber1.xaml.cs
public partial class PageNumber1 : ContentPage
{
public PageNumber1(List<Activity> activityList)
{
InitializeComponent();
if (activityList == null)
{
ActivityList.IsVisible = false;
}
else
{
for (int i = 0; i < activityList.Count; i++)
{
if (activityList[i].Title == "PageNumber1")
{
activityList[i].TextColor = Color.FromHex("#26b4d8");
activityList[i].Visibility = true;
}
else
{
activityList[i].TextColor = Color.Gray;
activityList[i].Visibility = false;
}
}
ActivityList.ItemsSource = activityList;
}
}
public void TagItemTapped(object sender, SelectionChangedEventArgs e)
{
var selectedItem = (e.CurrentSelection.FirstOrDefault() as Activity);
if (selectedItem != null)
{
string childnumber = "";
if (selectedItem.Title == "PageNumber1")
{
childnumber = "0";
}
else if (selectedItem.Title == "PageNumber2")
{
childnumber = "1";
}
else if (selectedItem.Title == "PageNumber3")
{
childnumber = "2";
}
else if (selectedItem.Title == "PageNumber4")
{
childnumber = "3";
}
else if (selectedItem.Title == "PageNumber5")
{
childnumber = "4";
}
MessagingCenter.Send<App, string>((App)Xamarin.Forms.Application.Current, "child", childnumber);
}
}
}
I have added the same code on all the other child pages with the corresponding title in the if statement. But the selected page title color is not working and underline is not showing.
Screenshot:
Also if I select the last item in the collectionview, I need to scroll the collection on the last child to the last item. For this I have used ScrollTo feature of Collectioview. But that is also not working.
protected override void OnAppearing()
{
ActivityList.ScrollTo(4);
}
The above code will work if I manually swipe the pages. When directly tap the collectionview item, the scrolling is not working.
I have uploaded a sample project here.
About underline not showing , the reason is HeightRequest of CollectionView setted too small with 30 .
Modify that to above 35 , it will show correcttly . Such as :
<CollectionView.HeightRequest>
<OnIdiom x:TypeArguments="x:Double">
<OnIdiom.Phone>40</OnIdiom.Phone>
<OnIdiom.Tablet>60</OnIdiom.Tablet>
<OnIdiom.Desktop>30</OnIdiom.Desktop>
</OnIdiom>
</CollectionView.HeightRequest>
The effect :
About selected problem , this is the sample project here .

Two search bar Xamarin Forms

I have a question here (I imagine it is for beginners: P).
I have a listview and a searchbar already working. I can make the right filter.
Then my question comes in, this listview has more than one column.
I can't think of a way to make 2 complementary filters.
For example:
Filter column 1 and then filter the result of that initial filter by column 2 (with another searchbar), that is, a filter on top of another filter.
My ListViewItem is like this with the filter:
C#
void InitList()
{
Items = new List<ListViewItem>
{
new ListViewItem { Name = "Guilherme", Bairro = "BOTAFOGO"},
new ListViewItem { Name = "João", Bairro = "FLAMENGO"},
new ListViewItem { Name = "Maria", Bairro = "CENTRO"}
}
}
void InitSearchBarBloco()
{
sb_search_bloco.TextChanged += (s, e) => FilterItem(sb_search_bloco.Text);
sb_search_bloco.SearchButtonPressed += (s, e) =>
FilterItem(sb_search_bloco.Text);
}
private void FilterItem(string filter)
{
exampleListView.BeginRefresh();
if (string.IsNullOrWhiteSpace(filter))
{
exampleListView.ItemsSource = Items;
}
else
{
exampleListView.ItemsSource = Items.Where(x => x.Name.ToLower().Contains(filter.ToLower()));
}
exampleListView.EndRefresh();
}
XAML
<SearchBar x:Name="sb_search_bloco" Placeholder="Nome..." />
<ListView x:Name="exampleListView" RowHeight="22" SelectedItem="{Binding Name}">
<ListView.ItemTemplate>
<DataTemplate>
<ViewCell >
<Grid>
<Label Text="{Binding Name}" LineBreakMode="TailTruncation" />
<Label Grid.Column="1" Text="{Binding Bairro}" />
</Grid>
</ViewCell>
</DataTemplate>
</ListView.ItemTemplate>
</ListView>
With this structure I can implement this ... "filtrate filter"?
thanks
I guess the below code should do fine for you. You just need to alter your linq code inside the FilterItem(string filter) method to achieve your requirement.
Note:
I have used OR condition inside the where clause to search if the enter text is available in both Name and Bairro. However, you can modify the condition as you require.
private void FilterItem(string filter)
{
exampleListView.BeginRefresh();
if (string.IsNullOrWhiteSpace(filter))
{
exampleListView.ItemsSource = Items;
}
else
{
//Alter the condition like below or based on requirement to achieve the desired result.
exampleListView.ItemsSource = Items.Where(x => x.Name.ToLower().Contains(filter.ToLower()) || x.Bairro.ToLower().Contains(filter.ToLower()));
}
exampleListView.EndRefresh();
}
I make a sample code for your referece.
xmal:
<StackLayout>
<StackLayout>
<SearchBar x:Name="sb_search_bloco" Placeholder="Nome..." TextChanged="sb_search_bloco_TextChanged"/>
<SearchBar x:Name="searchBar2" Margin="0,10" TextChanged="searchBar2_TextChanged" />
</StackLayout>
<ListView
x:Name="exampleListView"
RowHeight="22"
ItemsSource="{Binding Items}">
<ListView.ItemTemplate>
<DataTemplate>
<ViewCell>
<Grid>
<Label LineBreakMode="TailTruncation" Text="{Binding Name}" />
<Label Grid.Column="1" Text="{Binding Bairro}" />
</Grid>
</ViewCell>
</DataTemplate>
</ListView.ItemTemplate>
</ListView>
</StackLayout>
ListViewItem.cs
public class ListViewItem
{
public string Name { get; set; }
public string Bairro { get; set; }
}
MainPage.xml.cs
public partial class MainPage : ContentPage
{
public List<ListViewItem> Items { get; set; }
public MainPage()
{
InitializeComponent();
Items = new List<ListViewItem>
{
new ListViewItem { Name = "AAAA", Bairro = "BBCFS"},
new ListViewItem { Name = "ABBB", Bairro = "SSDCA"},
new ListViewItem { Name = "AAAA", Bairro = "AAAD"},
new ListViewItem { Name = "CCCC", Bairro = "SSS"},
new ListViewItem { Name = "DAAB", Bairro = "CCC"},
new ListViewItem { Name = "DDDC", Bairro = "QWDAS"}
};
this.BindingContext = this;
}
private void sb_search_bloco_TextChanged(object sender, TextChangedEventArgs e)
{
exampleListView.ItemsSource = FilterItem1(e.NewTextValue);
}
IEnumerable<ListViewItem> FilterItem1(string filter = null)
{
if (string.IsNullOrEmpty(filter))
return Items;
return Items.Where(p => p.Name.StartsWith(filter));
}
private void searchBar2_TextChanged(object sender, TextChangedEventArgs e)
{
exampleListView.ItemsSource = FilterItem2(e.NewTextValue);
}
IEnumerable<ListViewItem> FilterItem2(string filter = null)
{
if (string.IsNullOrEmpty(filter))
return Items;
return Items.Where(p => p.Bairro.StartsWith(filter));
}
}

Highlight URL using label span - xamarin.forms

I am creating a chat application in xamarin.forms.What I am trying to achieve is whenever user typed message contains a URL, that should be highlighted and provide click to it.For this feature I found Span in Label text.When user click on send button of chat , I will check for URL and make it as another span.I got this idea from Lucas Zhang - MSFT form this question here.
The problem is I am trying to do the spanning in view model and the individual chat bubble is in another view cell which will call as ItemTemplate in my chat listview. Anyway the spanning is not working as I intended ie; it doesn't highlight .
My view Model.
public Queue<Message> DelayedMessages { get; set; } = new Queue<Message>();
public ObservableCollection<Message> Messages { get; set; } = new ObservableCollection<Message>();
public string TextToSend { get; set; }
public ChatPageViewModel()
{
OnSendCommand = new Command(() =>
{
if (!string.IsNullOrEmpty(TextToSend))
{
var urlStr = TextToSend;
int startIndex = 0, endIndex = 0;
if (urlStr.Contains("www."))
{
startIndex = urlStr.IndexOf("www.");
}
if (urlStr.Contains(".com"))
{
endIndex = urlStr.IndexOf(".com") + 3;
}
if (startIndex != 0 || endIndex != 0)
{
var formattedString = new FormattedString();
Span span1 = new Span() { Text = urlStr.Substring(0, startIndex), TextColor = Color.Black };
formattedString.Spans.Add(span1);
Span span2 = new Span() { Text = urlStr.Substring(startIndex, endIndex - startIndex + 1), TextColor = Color.LightBlue };
span2.GestureRecognizers.Add(new TapGestureRecognizer()
{
NumberOfTapsRequired = 1,
Command = new Command(() => {
})
});
formattedString.Spans.Add(span2);
Span span3 = new Span() { Text = urlStr.Substring(endIndex, urlStr.Length - 1 - endIndex), TextColor = Color.Black };
formattedString.Spans.Add(span3);
var message = new Message
{
Text = formattedString.ToString(),
IsIncoming = false,
MessageDateTime = DateTime.Now
};
Messages.Add(message);
TextToSend = string.Empty;
}
else
{
var message = new Message
{
Text = urlStr.ToString(),
IsIncoming = false,
MessageDateTime = DateTime.Now
};
Messages.Add(message);
TextToSend = string.Empty;
}
}
});
}
Single chat Bubble XAML
<Label x:Name="OutgoingMessage" TextColor="White" FormattedText="{Binding Text}" HorizontalOptions="End" >
</Label>
My Chat page XAML
<Grid RowSpacing="0" Margin="0,20,0,0"
ColumnSpacing="0">
<Grid.RowDefinitions>
<RowDefinition Height="*" />
<RowDefinition Height="1" />
<RowDefinition Height="Auto" />
</Grid.RowDefinitions>
<ListView Grid.Row="0"
ItemTemplate="{StaticResource MessageTemplateSelector}"
ItemsSource="{Binding Messages,Mode=OneWay}"
Margin="0"
SelectionMode="None"
FlowDirection="RightToLeft"
HasUnevenRows="True" x:Name="ChatList"
VerticalOptions="FillAndExpand"
SeparatorColor="Transparent"
>
</ListView>
<BoxView HorizontalOptions="FillAndExpand"
HeightRequest="1"
BackgroundColor="#F2F3F5"
Grid.Row="1"/>
<partials:ChatInputBarView Grid.Row="2"
Margin="0,0,0,0"
x:Name="chatInput"/>
</Grid>
ChatPage.xaml.cs
public partial class ChatPage : ContentPage
{
ChatPageViewModel vm;
public ChatPage()
{
InitializeComponent();
this.BindingContext = vm= new ChatPageViewModel();
}
}
Messages class
public class Message : ObservableObject
{
string text;
public string Text
{
get { return text; }
set { SetProperty(ref text, value); }
}
DateTime messageDateTime;
public DateTime MessageDateTime
{
get { return messageDateTime; }
set { SetProperty(ref messageDateTime, value); }
}
public string MessageTimeDisplay => MessageDateTime.Humanize();
bool isIncoming;
public bool IsIncoming
{
get { return isIncoming; }
set { SetProperty(ref isIncoming, value); }
}
}
Any Help is appreciated.
EDIT:
This question was actually continuation of question. Previously I used AwesomeHyperLinkLabel fromlink. The problem was I cant manage the click event of that label.Thats why I moved with label span.Thanks to Leo Zhu - MSFT For the render changes.
For Android:
[assembly: ExportRenderer(typeof(AwesomeHyperLinkLabel), typeof(AwesomeHyperLinkLabelRenderer))]
namespace App18.Droid
{
public class AwesomeHyperLinkLabelRenderer : LabelRenderer
{
public AwesomeHyperLinkLabelRenderer(Context context) : base(context)
{
}
protected override void OnElementChanged(ElementChangedEventArgs<Label> e)
{
base.OnElementChanged(e);
var view = (AwesomeHyperLinkLabel)Element;
if (view == null) return;
TextView textView = new TextView(Forms.Context);
textView.LayoutParameters = new LayoutParams(LayoutParams.WrapContent, LayoutParams.WrapContent);
textView.SetTextColor(view.TextColor.ToAndroid());
// Setting the auto link mask to capture all types of link-able data
textView.AutoLinkMask = MatchOptions.All;
// Make sure to set text after setting the mask
textView.Text = view.Text;
AddHyperlinksManually(textView);
//textView.SetTextSize(ComplexUnitType.Dip, (float)view.FontSize);
// overriding Xamarin Forms Label and replace with our native control
SetNativeControl(textView);
}
public static void AddHyperlinksManually(TextView _tv)
{
SpannableStringBuilder currentSpan = new SpannableStringBuilder(_tv.Text);
Linkify.AddLinks(currentSpan, MatchOptions.WebUrls);
var objects = currentSpan.GetSpans(0, currentSpan.Length(), Java.Lang.Class.FromType(typeof(URLSpan)));
var urlSpans = new URLSpan[objects.Length];
for (var i = 0; i < urlSpans.Length; i++)
{
urlSpans[i] = objects[i] as URLSpan;
}
foreach (URLSpan _url in urlSpans)
{
int iStart = currentSpan.GetSpanStart(_url);
int iEnd = currentSpan.GetSpanEnd(_url);
currentSpan.RemoveSpan(_url);
currentSpan.SetSpan(new CustomURLSpan(_url.URL), iStart, iEnd, SpanTypes.InclusiveInclusive);
_tv.SetText(currentSpan, TextView.BufferType.Normal);
_tv.MovementMethod = LinkMovementMethod.Instance;
}
}
public class CustomURLSpan : ClickableSpan
{
string mTargetURL;
public CustomURLSpan(string _url) {
mTargetURL =_url;
}
public override void OnClick(Android.Views.View widget)
{
//here you could handle the click event,and you could use MessagingCenter to send mTargetURL to your Page.
Console.WriteLine("Click");
}
}
}
The mistake was with my model.Changed string to FormattedString and also changed in the viewmodel
public class Message : ObservableObject
{
FormattedString text;
public FormattedString Text
{
get { return text; }
set { SetProperty(ref text, value); }
}
DateTime messageDateTime;
public DateTime MessageDateTime
{
get { return messageDateTime; }
set { SetProperty(ref messageDateTime, value); }
}
public string MessageTimeDisplay => MessageDateTime.Humanize();
bool isIncoming;
public bool IsIncoming
{
get { return isIncoming; }
set { SetProperty(ref isIncoming, value); }
}
}

How to fix blink issue Create Checkbox using image in Xamarin Forms?

This is the CheckBox.xaml
<?xml version="1.0" encoding="UTF-8"?>
<ContentView xmlns="http://xamarin.com/schemas/2014/forms"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
x:Class="CheckBoxSample.Controls.CheckBox">
<ContentView.Content>
<StackLayout Orientation="Horizontal"
x:Name="mainContainer"
HorizontalOptions="FillAndExpand"
VerticalOptions="FillAndExpand"
Padding="0"
Spacing="5">
<AbsoluteLayout HorizontalOptions="Center"
VerticalOptions="Center"
WidthRequest="20"
HeightRequest="20"
x:Name="imageContainer">
<Image Source="{Binding CheckedBackgroundImageSource}"
x:Name="checkedBackground"
Aspect="AspectFit"
AbsoluteLayout.LayoutBounds="0.5, 0.5, 1, 1"
AbsoluteLayout.LayoutFlags="All"
Opacity="0"
InputTransparent="True"/>
<Image Source="{Binding BorderImageSource}"
x:Name="borderImage"
Aspect="AspectFit"
AbsoluteLayout.LayoutBounds="0.5, 0.5, 1, 1"
AbsoluteLayout.LayoutFlags="All"
InputTransparent="True"/>
<Image Source="{Binding CheckmarkImageSource}"
x:Name="checkedImage"
Aspect="AspectFit"
AbsoluteLayout.LayoutBounds="0.5, 0.5, 1, 1"
AbsoluteLayout.LayoutFlags="All"
Opacity="0"
InputTransparent="True"/>
</AbsoluteLayout>
<Label x:Name="controlLabel"
HorizontalOptions="FillAndExpand"
VerticalOptions="FillAndExpand"
HorizontalTextAlignment="Start"
VerticalTextAlignment="Center"
Text="{Binding Title}"
Style="{Binding LabelStyle}"
InputTransparent="True"/>
</StackLayout>
</ContentView.Content>
</ContentView>
This is CheckBox.Xaml.cs .
using System;
using System.Collections.Generic;
using Xamarin.Forms;
using Xamarin.Forms.Xaml;
namespace CheckBoxSample.Controls
{ /// <summary>
/// Custom checkbox control
/// </summary>
[XamlCompilation(XamlCompilationOptions.Compile)]
public partial class CheckBox : ContentView
{
public CheckBox()
{
InitializeComponent();
controlLabel.BindingContext = this;
checkedBackground.BindingContext = this;
checkedImage.BindingContext = this;
borderImage.BindingContext = this;
mainContainer.GestureRecognizers.Add(new TapGestureRecognizer()
{
Command = new Command(tapped)
});
}
public static readonly BindableProperty BorderImageSourceProperty = BindableProperty.Create(nameof(BorderImageSource), typeof(string), typeof(CheckBox), "", BindingMode.OneWay);
public static readonly BindableProperty CheckedBackgroundImageSourceProperty = BindableProperty.Create(nameof(CheckedBackgroundImageSource), typeof(string), typeof(CheckBox), "", BindingMode.OneWay);
public static readonly BindableProperty CheckmarkImageSourceProperty = BindableProperty.Create(nameof(CheckmarkImageSource), typeof(string), typeof(CheckBox), "", BindingMode.OneWay);
public static readonly BindableProperty IsCheckedProperty = BindableProperty.Create(nameof(IsChecked), typeof(bool), typeof(CheckBox), false, BindingMode.TwoWay, propertyChanged: checkedPropertyChanged);
public static readonly BindableProperty TitleProperty = BindableProperty.Create(nameof(Title), typeof(string), typeof(CheckBox), "", BindingMode.OneWay);
public static readonly BindableProperty CheckedChangedCommandProperty = BindableProperty.Create(nameof(CheckedChangedCommand), typeof(Command), typeof(CheckBox), null, BindingMode.OneWay);
public static readonly BindableProperty LabelStyleProperty = BindableProperty.Create(nameof(LabelStyle), typeof(Style), typeof(CheckBox), null, BindingMode.OneWay);
public string BorderImageSource
{
get { return (string)GetValue(BorderImageSourceProperty); }
set { SetValue(BorderImageSourceProperty, value); }
}
public string CheckedBackgroundImageSource
{
get { return (string)GetValue(CheckedBackgroundImageSourceProperty); }
set { SetValue(CheckedBackgroundImageSourceProperty, value); }
}
public string CheckmarkImageSource
{
get { return (string)GetValue(CheckmarkImageSourceProperty); }
set { SetValue(CheckmarkImageSourceProperty, value); }
}
public bool IsChecked
{
get { return (bool)GetValue(IsCheckedProperty); }
set { SetValue(IsCheckedProperty, value); }
}
public string Title
{
get { return (string)GetValue(TitleProperty); }
set { SetValue(TitleProperty, value); }
}
public Command CheckedChangedCommand
{
get { return (Command)GetValue(CheckedChangedCommandProperty); }
set { SetValue(CheckedChangedCommandProperty, value); }
}
public Style LabelStyle
{
get { return (Style)GetValue(LabelStyleProperty); }
set { SetValue(LabelStyleProperty, value); }
}
public Label ControlLabel
{
get { return controlLabel; }
}
static void checkedPropertyChanged(BindableObject bindable, object oldValue, object newValue)
{
((CheckBox)bindable).ApplyCheckedState();
}
/// <summary>
/// Handle chackox tapped action
/// </summary>
void tapped()
{
IsChecked = !IsChecked;
ApplyCheckedState();
}
/// <summary>
/// Reflect the checked event change on the UI
/// with a small animation
/// </summary>
/// <param name="isChecked"></param>
///
void ApplyCheckedState()
{
Animation storyboard = new Animation();
Animation fadeAnim = null;
Animation checkBounceAnim = null;
Animation checkFadeAnim = null;
double fadeStartVal = 0;
double fadeEndVal = 1;
double scaleStartVal = 0;
double scaleEndVal = 1;
Easing checkEasing = Easing.CubicIn;
if (IsChecked)
{
checkedImage.Scale = 0;
fadeStartVal = 0;
fadeEndVal = 1;
scaleStartVal = 0;
scaleEndVal = 1;
checkEasing = Easing.CubicIn;
}
else
{
fadeStartVal = 1;
fadeEndVal = 0;
scaleStartVal = 1;
scaleEndVal = 0;
checkEasing = Easing.CubicOut;
}
fadeAnim = new Animation(
callback: d => checkedBackground.Opacity = d,
start: fadeStartVal,
end: fadeEndVal,
easing: Easing.CubicOut
);
checkFadeAnim = new Animation(
callback: d => checkedImage.Opacity = d,
start: fadeStartVal,
end: fadeEndVal,
easing: checkEasing
);
checkBounceAnim = new Animation(
callback: d => checkedImage.Scale = d,
start: scaleStartVal,
end: scaleEndVal,
easing: checkEasing
);
storyboard.Add(0, 0.6, fadeAnim);
storyboard.Add(0, 0.6, checkFadeAnim);
storyboard.Add(0.4, 1, checkBounceAnim);
storyboard.Commit(this, "checkAnimation", length: 600);
if (CheckedChangedCommand != null && CheckedChangedCommand.CanExecute(this))
CheckedChangedCommand.Execute(this);
}
}
}
Use custom checkbox in Xamarin.Forms
declare namespace of CheckBox.
xmlns:ctrls="clr-namespace:CheckBoxSample.Controls"
CheckBox like below
<ctrls:CheckBox x:Name="cbIndia" Title="India" IsChecked="True" BorderImageSource="checkborder" CheckedBackgroundImageSource="checkcheckedbg" CheckmarkImageSource="checkcheckmark" />
Set the values for the CheckBox properties are
Title: India
IsChecked: True
BorderImageSource: checkborder.png
CheckBackgroundImageSource: checkcheckedbg.png
CheckmarkImageSource: checkcheckmark.png
Demo Screen for Android.
Demo Screen for IOS.
Try this:
<ctrls:CheckBox x:Name="cbIndia" Title="India" IsChecked="False" BorderImageSource="checkborder" CheckedBackgroundImageSource="checkcheckedbg" CheckmarkImageSource="checkcheckmark" />

How can I implement a click to select using a ViewModel

I have this XAML that displays 6 TextCells which can show a check mark or not. They also so as enabled or not enabled:
<TableSection Title="Front Side" x:Name="cfsSection">
<local:CustomTextCell Text="{Binding [0].Name}" IsChecked="{Binding [0].IsSelected}" IsEnabled="{Binding [0].IsSelected, Converter={StaticResource InverseBoolConverter} } "Tapped="cfsSelectValue"/>
<local:CustomTextCell Text="{Binding [1].Name}" IsChecked="{Binding [1].IsSelected}" IsEnabled="{Binding [1].IsSelected, Converter={StaticResource InverseBoolConverter} } "Tapped="cfsSelectValue"/>
<local:CustomTextCell Text="{Binding [2].Name}" IsChecked="{Binding [2].IsSelected}" IsEnabled="{Binding [2].IsSelected, Converter={StaticResource InverseBoolConverter} } "Tapped="cfsSelectValue"/>
<local:CustomTextCell Text="{Binding [3].Name}" IsChecked="{Binding [3].IsSelected}" IsEnabled="{Binding [3].IsSelected, Converter={StaticResource InverseBoolConverter} } "Tapped="cfsSelectValue"/>
<local:CustomTextCell Text="{Binding [4].Name}" IsChecked="{Binding [4].IsSelected}" IsEnabled="{Binding [4].IsSelected, Converter={StaticResource InverseBoolConverter} } "Tapped="cfsSelectValue"/>
<local:CustomTextCell Text="{Binding [5].Name}" IsChecked="{Binding [5].IsSelected}" IsEnabled="{Binding [5].IsSelected, Converter={StaticResource InverseBoolConverter} } "Tapped="cfsSelectValue"/>
</TableSection>
The code behind is I think fairly simple. It declares an array of SSVViewModel and the binding causes the text to display:
SSVViewModel[] CFS = new[] {
new SSVViewModel {Id = 0, Name=LANG.English.Text(), IsSelected = false},
new SSVViewModel {Id = 1, Name=LANG.Romaji.Text(), IsSelected = false},
new SSVViewModel {Id = 2, Name=LANG.Kana.Text(), IsSelected = false},
new SSVViewModel {Id = 3, Name=LANG.Kanji.Text(), IsSelected = false},
new SSVViewModel {Id = 4, Name=LANG.KanjiKana.Text(), IsSelected = false},
new SSVViewModel {Id = 5, Name=LANG.KanaKanji.Text(), IsSelected = false},
};
When a cell is clicked this function is called:
void cfsSelectValue(object sender, EventArgs e)
{
var cell = sender as TextCell;
if (cell == null)
return;
var selected = cell.Text;
foreach (var setting in CFS)
setting.IsSelected = false;
foreach (var setting in CFS)
if (setting.Name == selected)
setting.IsSelected = true;
}
However when clicking on either of the first two cells both show as checked. All other cell clicks work fine. In another part of my code I use a similar construct and it's the last two cells that do not work.
Note that the IsEnabled works but not the IsChecked
Can anyone see why a click on the first two cells could possibly give any problem. I have been through this with the debugger many time but I still cannot see what might be wrong. Surely the code that sets the IsSelected to false should cause all except the one cell to show as checked.
Note that when debugging this line: setting.IsSelected = false; and this line: setting.IsSelected = true; then everything appears as it should as the correct cell has it's IsSelected set to true and the others to false. It's just when I look at the display it seems like the binding didn't work for those first two cells.
Here's the viewModel code I am using:
public class SSVViewModel: ObservableProperty
{
private int id;
private string name;
private bool isSelected;
public int Id
{
get
{
return id;
}
set
{
if (value != id)
{
id = value;
NotifyPropertyChanged("Id");
}
}
}
public string Name
{
get
{
return name;
}
set
{
if (value != name)
{
name = value;
NotifyPropertyChanged("Name");
}
}
}
public bool IsSelected
{
get
{
return isSelected;
}
set
{
if (value != isSelected)
{
isSelected = value;
NotifyPropertyChanged("IsSelected");
}
}
}
}
Here is the code for the CustomTextCellRenderer
public class CustomTextCellRenderer : TextCellRenderer
{
UITableViewCell _nativeCell;
public override UITableViewCell GetCell(Cell item, UITableViewCell reusableCell, UITableView tv)
{
_nativeCell = base.GetCell(item, reusableCell, tv);
var formsCell = item as CustomTextCell;
if (formsCell != null)
{
formsCell.PropertyChanged -= OnPropertyChanged;
formsCell.PropertyChanged += OnPropertyChanged;
}
SetCheckmark(formsCell);
SetTap(formsCell);
return _nativeCell;
}
void OnPropertyChanged(object sender, PropertyChangedEventArgs e)
{
var formsCell = sender as CustomTextCell;
if (formsCell == null)
return;
if (e.PropertyName == CustomTextCell.IsCheckedProperty.PropertyName)
{
SetCheckmark(formsCell);
}
if (e.PropertyName == CustomTextCell.NoTapProperty.PropertyName)
{
SetTap(formsCell);
}
}
private void SetCheckmark(CustomTextCell formsCell)
{
if (formsCell.IsChecked)
_nativeCell.Accessory = UITableViewCellAccessory.Checkmark;
else
_nativeCell.Accessory = UITableViewCellAccessory.None;
}
private void SetTap(CustomTextCell formsCell)
{
if (formsCell.NoTap)
_nativeCell.SelectionStyle = UITableViewCellSelectionStyle.None;
else
_nativeCell.SelectionStyle = UITableViewCellSelectionStyle.Default;
}
}
Update 1
<local:CustomTextCell Text="{Binding [0].Name}" IsChecked="{Binding [0].IsSelected}" Tapped="cfsSelectValue" CommandParameter="0" />
<local:CustomTextCell Text="{Binding [1].Name}" IsChecked="{Binding [1].IsSelected}" Tapped="cfsSelectValue" CommandParameter="1" />
<local:CustomTextCell Text="{Binding [2].Name}" IsChecked="{Binding [2].IsSelected}" Tapped="cfsSelectValue" CommandParameter="2" />
<local:CustomTextCell Text="{Binding [3].Name}" IsChecked="{Binding [3].IsSelected}" Tapped="cfsSelectValue" CommandParameter="3" />
<local:CustomTextCell Text="{Binding [4].Name}" IsChecked="{Binding [4].IsSelected}" Tapped="cfsSelectValue" CommandParameter="4" />
Since the question was written I have stopped using this for the Lang selection however it's still used in another part of the code and I tried to put in some debug points. Here's what I did:
I added this to the iOS custom renderer:
private void SetCheckmark(CustomTextCell formsCell)
{
if (formsCell.IsChecked)
{
_nativeCell.Accessory = UITableViewCellAccessory.Checkmark;
Debug.WriteLine(_nativeCell.TextLabel.Text + " checked");
}
else
{
_nativeCell.Accessory = UITableViewCellAccessory.None;
Debug.WriteLine(_nativeCell.TextLabel.Text + " unchecked");
}
}
Here's the result when I clicked JLPT N2:
Category Group unchecked
Category unchecked
All Available Words unchecked
Japanese for Busy People 1 unchecked
Japanese for Busy People 2 unchecked
Japanese for Busy People 3 unchecked
JLPT Level N5 unchecked
JLPT Level N4 unchecked
JLPT Level N3 unchecked
JLPT Level N2 unchecked
JLPT Level N1 checked
JLPT Level N2 checked
JLPT Level N3 checked
JLPT Level N2 checked
JLPT Level N2 checked
JLPT Level N2 checked
JLPT Level N2 checked
JLPT Level N2 checked
JLPT Level N2 checked
JLPT Level N2 checked
JLPT Level N2 checked
JLPT Level N2 checked
JLPT Level N2 checked
JLPT Level N2 checked
JLPT Level N2 checked
JLPT Level N2 checked
JLPT Level N2 checked
JLPT Level N2 checked
JLPT Level N1 unchecked
JLPT Level N1 unchecked
JLPT Level N1 unchecked
JLPT Level N1 unchecked
JLPT Level N1 unchecked
JLPT Level N1 unchecked
JLPT Level N1 unchecked
JLPT Level N1 unchecked
JLPT Level N1 unchecked
JLPT Level N1 unchecked
JLPT Level N1 unchecked
JLPT Level N1 unchecked
JLPT Level N1 unchecked
JLPT Level N1 unchecked
JLPT Level N1 unchecked
This was not at all what I expected.
On the screen I see that as expected N2 is disabled but there is a check mark next to N3 and N2.
Not sure if this helps but I notice the iOS renderer code used is different from similar code I have used in other places. For example here's a different iOS renderer. Code looks very different. I realize function is different but this one has things like cell = tv.DequeueReusableCell(fullName) as CellTableViewCell;
public class TextCellCustomRenderer : TextCellRenderer
{
CellTableViewCell cell;
public override UITableViewCell GetCell(Cell item, UITableViewCell reusableCell, UITableView tv)
{
var textCell = (TextCell)item;
var fullName = item.GetType().FullName;
cell = tv.DequeueReusableCell(fullName) as CellTableViewCell;
if (cell == null)
{
cell = new CellTableViewCell(UITableViewCellStyle.Value1, fullName);
}
else
{
cell.Cell.PropertyChanged -= cell.HandlePropertyChanged;
//cell.Cell.PropertyChanged -= Current_PropertyChanged;
}
cell.Cell = textCell;
textCell.PropertyChanged += cell.HandlePropertyChanged;
cell.PropertyChanged = this.HandlePropertyChanged;
cell.SelectionStyle = UITableViewCellSelectionStyle.None;
cell.TextLabel.Text = textCell.Text;
cell.DetailTextLabel.Text = textCell.Detail;
cell.ContentView.BackgroundColor = UIColor.White;
switch (item.StyleId)
{
case "checkmark":
cell.Accessory = UIKit.UITableViewCellAccessory.Checkmark;
break;
case "detail-button":
cell.Accessory = UIKit.UITableViewCellAccessory.DetailButton;
break;
case "detail-disclosure-button":
cell.Accessory = UIKit.UITableViewCellAccessory.DetailDisclosureButton;
break;
case "disclosure":
cell.Accessory = UIKit.UITableViewCellAccessory.DisclosureIndicator;
break;
case "none":
default:
cell.Accessory = UIKit.UITableViewCellAccessory.None;
break;
}
//UpdateBackground(cell, item);
return cell;
}
void checkAccessoryVisibility() {
}
}
As per the log statements, it looks like the unsubscribe logic for property-changed-handler is not working as expected.
With TextCellRenderer, we don't need to explicitly subscribe to property-changed-event as there is a base overridable method HandlePropertyChanged that can be re-used in this context.
Changing the renderer code to use this method (similar to this answer) should hopefully resolve this problem:
public class CustomTextCellRenderer : TextCellRenderer
{
public override UITableViewCell GetCell(Cell item, UITableViewCell reusableCell, UITableView tv)
{
var nativeCell = base.GetCell(item, reusableCell, tv);
if (item is CustomTextCell formsCell)
{
SetCheckmark(nativeCell, formsCell);
SetTap(nativeCell, formsCell);
}
return nativeCell;
}
protected override void HandlePropertyChanged(object sender, PropertyChangedEventArgs args)
{
base.HandlePropertyChanged(sender, args);
System.Diagnostics.Debug.WriteLine($"HandlePropertyChanged {args.PropertyName}");
var nativeCell = sender as CellTableViewCell;
if (nativeCell?.Element is CustomTextCell formsCell)
{
if (args.PropertyName == CustomTextCell.IsCheckedProperty.PropertyName)
SetCheckmark(nativeCell, formsCell);
else if (args.PropertyName == CustomTextCell.NoTapProperty.PropertyName)
SetTap(nativeCell, formsCell);
}
}
void SetCheckmark(UITableViewCell nativeCell, CustomTextCell formsCell)
{
if (formsCell.IsChecked)
nativeCell.Accessory = UITableViewCellAccessory.Checkmark;
else
nativeCell.Accessory = UITableViewCellAccessory.None;
}
void SetTap(UITableViewCell nativeCell, CustomTextCell formsCell)
{
if (formsCell.NoTap)
_nativeCell.SelectionStyle = UITableViewCellSelectionStyle.None;
else
_nativeCell.SelectionStyle = UITableViewCellSelectionStyle.Default;
}
}
I could not find the error causing that behaviour so here are my thoughts on this:
The behaviour you are describing sounds more like a RadioButton. Since there are none in Xamarin.Forms you could create your own, use a package or get a workaround.
Most easy workaround would be in your cfsSelectValue.
You could search the visual tree for all elements of local:CustomTextCells and set IsSelected to false for each TextCell that is not the one passed as the sender.
How I would do it:
Things to try out:
For your ViewModels use an ObservableCollection rather than an Array
private ObservableCollection<SSVViewModel> viewModels = new ObservableCollection<SSVViewModel>()
{
new SSVViewModel {Id = 0, Name=LANG.English.Text()},
new SSVViewModel {Id = 1, Name=LANG.Romaji.Text()},
new SSVViewModel {Id = 2, Name=LANG.Kana.Text()},
new SSVViewModel {Id = 3, Name=LANG.Kanji.Text()},
new SSVViewModel {Id = 4, Name=LANG.KanjiKana.Text()},
new SSVViewModel {Id = 5, Name=LANG.KanaKanji.Text()},
};
Derive your ViewModel from INotifyPropertyChanged rather than ObservableProperty
public class SSVViewModel : INotifyPropertyChanged
{
private int id;
private string name;
private bool isSelected = false; //Set the default here
public int Id
{
get
{
return id;
}
set
{
if (value != id)
{
id = value;
OnPropertyChanged();
}
}
}
public string Name
{
get
{
return name;
}
set
{
if (value != name)
{
name = value;
OnPropertyChanged();
}
}
}
public bool IsSelected
{
get
{
return isSelected;
}
set
{
if (value != isSelected)
{
isSelected = value;
OnPropertyChanged();
}
}
}
#region Implementation of INotifyPropertyChanged
public event PropertyChangedEventHandler PropertyChanged;
private void OnPropertyChanged([CallerMemberName] string propertyname = null)
{
if(PropertyChanged != null)
{
PropertyChanged.Invoke(this, new PropertyChangedEventArgs(propertyname));
}
}
#endregion
}
Change your cfsSelectValue to this:
public void cfsSelectValue(object sender, EventArgs e)
{
//GetCurrentCell
CustomTextCell cell = sender as CustomTextCell;
if (cell == null)
{
return;
}
foreach (SSVViewModel viewModel in CFS)
{
/*
Since there is no Tag Property we gotta use something different
you could use `CommandParameter` since it is of type object
*/
if (viewModel.Name == cell.Text && viewModel.Id == int.Parse(cell.CommandParameter.ToString()))
{
viewModel.IsSelected = true;
}
else
{
viewModel.IsSelected = false;
}
}
}
The code looks right to me - the only reason I would imagine this happening is in case the string comparison logic is not working as expected.
Maybe a reference based comparison might solve the issue. i.e Change your XAML to:
<TableSection Title="Front Side" x:Name="cfsSection">
<local:CustomTextCell BindingContext="{Binding [0]}" Text="{Binding Name}" IsChecked="{Binding IsSelected}" Tapped="cfsSelectValue"/>
<local:CustomTextCell BindingContext="{Binding [1]}" Text="{Binding Name}" IsChecked="{Binding IsSelected}" Tapped="cfsSelectValue"/>
<local:CustomTextCell BindingContext="{Binding [2]}" Text="{Binding Name}" IsChecked="{Binding IsSelected}" Tapped="cfsSelectValue"/>
<local:CustomTextCell BindingContext="{Binding [3]}" Text="{Binding Name}" IsChecked="{Binding IsSelected}" Tapped="cfsSelectValue"/>
<local:CustomTextCell BindingContext="{Binding [4]}" Text="{Binding Name}" IsChecked="{Binding IsSelected}" Tapped="cfsSelectValue"/>
<local:CustomTextCell BindingContext="{Binding [5]}" Text="{Binding Name}" IsChecked="{Binding IsSelected}" Tapped="cfsSelectValue"/>
</TableSection>
and tapped handler to:
void cfsSelectValue(object sender, EventArgs e)
{
var cell = sender as TextCell;
if (cell == null)
return;
var selected = cell.BindingContext;
foreach (var setting in CFS)
setting.IsSelected = (setting == selected);
}

Resources