How can I implement a click to select using a ViewModel - xamarin

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

Related

Avalonia Dynamic Tree View MVVM

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!

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 .

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

WP7 - VisualTreeHelper to loop through all ListBox Items

I need to create a new ListBox based on items that are selected (checked). The following code actually worked if I only had like 20 items on the ListBox, but adding more items make it crash. Can anybody know how to make it work, or have a different aproach? Is there a limite for looping through a listBox?
// worked fine for 20 items,
// but my actual list contems 95 items...
private void btnCreateNewList_Click(object sender, RoutedEventArgs e)
{
int totalItemsCB = ListCheckBoxVocabulary.Items.Count;
for (int ii = 0; ii < totalItemsCB-1; ii++)
{
ListBoxItem item = this.ListCheckBoxVocabulary.ItemContainerGenerator.ContainerFromIndex(ii) as ListBoxItem;
CheckBox thisCheckBox = FindFirstElementInVisualTree<CheckBox>(item);
if (thisCheckBox.IsChecked == true)
{
dataPlayListSource.Add(new SampleData() { Text = thisCheckBox.Content.ToString() + " | " + ii });
// this.PlayListCheckBoxVocabulary.UpdateLayout();
this.PlayListCheckBoxVocabulary.ItemsSource = dataPlayListSource;
}
}
}
private T FindFirstElementInVisualTree<T>(DependencyObject parentElement) where T : DependencyObject
{
var count = VisualTreeHelper.GetChildrenCount(parentElement);
if (count == 0)
return null;
for (int i = 0; i < count; i++)
{
var child = VisualTreeHelper.GetChild(parentElement, i);
if (child != null && child is T)
{
return (T)child;
}
else
{
var result = FindFirstElementInVisualTree<T>(child);
if (result != null)
return result;
}
}
return null;
}
and xaml:
<controls:PivotItem Header="Vocabulary" >
<ListBox x:Name="ListCheckBoxVocabulary" Margin="0,0,-12,0" ItemsSource="{Binding Items}">
<ListBox.ItemTemplate>
<DataTemplate>
<!--<StackPanel Margin="0,0,0,17" Width="432">-->
<CheckBox x:Name="cbVocabulary" Content="{Binding Text}" Checked="CheckBox_Checked" Unchecked="UncheckBox" />
<!--</StackPanel>-->
</DataTemplate>
</ListBox.ItemTemplate>
</ListBox>
</controls:PivotItem>
The list is virtual - controls are created as they are needed and potentially reused (I think).
Your options are to turn the ListBox to not be virtualized (override the template, and for the container, instead of a SerializedStackPanel, choose a regular StackPanel).
Your other (and preferable) option is to do the checking via Data Binding. Way easier and faster in most situations.

How to get UIElement at specific position in ItemsControl in WP7?

I filled the icBoard with 50 Cell objects, so each Rectangle object has Cell as data object. Now, I want according to index or cell object to get the corresponding Rectangle element. For example I want to get the Rectangle in index=15. Not it's data but the Rectangle itself.
How I can do this?
public MainPage()
{
InitializeComponent();
var cells = new List<Cell>();
for (int i = 0; i < 50; i++)
{
cells.Add(new Cell());
}
icCells.ItemsSource = cells;
}
public void sector_Tap(object sender, System.Windows.Input.GestureEventArgs e)
{
//some code
//....
var tappedRectangle = (sender as Rectangle);
var spesificRectangle = SOMEHOW_GET_RECTANGLE_AT_POSITION_15;
}
<ItemsControl Name="icBoard" Grid.Column="0" Margin="0">
<ItemsControl.ItemTemplate>
<DataTemplate>
<Rectangle Fill="#501e4696" Width="30" Height="30" Margin="1" Tap="sector_Tap" />
</DataTemplate>
</ItemsControl.ItemTemplate>
<ItemsControl.ItemsPanel>
<ItemsPanelTemplate>
<toolkit:WrapPanel />
</ItemsPanelTemplate>
</ItemsControl.ItemsPanel>
</ItemsControl>
I believe this might work:
ContentPresenter contentPresenter = itemsControl.ItemContainerGenerator.ContainerFromIndex(15) as ContentPresenter;
Rectangle rectangle= FindVisualChild<Rectangle>(contentPresenter );
if (rectangle != null)
{
}
public static T FindVisualChild<T>(DependencyObject depObj) where T : DependencyObject
{
if (depObj != null)
{
for (int i = 0; i < VisualTreeHelper.GetChildrenCount(depObj); i++)
{
DependencyObject child = VisualTreeHelper.GetChild(depObj, i);
if (child != null && child is T)
{
return (T)child;
}
T childItem = FindVisualChild<T>(child);
if (childItem != null) return childItem;
}
}
return null;
}

Resources