Xamarin Forms - finding databound object from custom renderer while responding to a swipe - xamarin

From an iOS swipe event, I am trying to figure out how to work my way back to the model databound to the ViewCell (model is my own Drive object, a simple POCO).
I am using a subclassed StackLayout ...
public class MainPageStackLayout : StackLayout { }
with a custom renderer...
[assembly: ExportRenderer(typeof(MainPageStackLayout), typeof(MainPageStackLayoutRenderer))]
namespace DriveLive.iOS
{
public class MainPageStackLayoutRenderer : VisualElementRenderer<StackLayout>
{
UISwipeGestureRecognizer swipeGestureRecognizer;
protected override void OnElementChanged(ElementChangedEventArgs<StackLayout> e)
{
base.OnElementChanged(e);
swipeGestureRecognizer = new UISwipeGestureRecognizer(() =>
{
//********************
Console.WriteLine("How to access the underlying model here?");
//********************
}) { Direction = UISwipeGestureRecognizerDirection.Left, NumberOfTouchesRequired = 1 };
if (e.NewElement == null)
{
if (swipeGestureRecognizer != null)
this.RemoveGestureRecognizer(swipeGestureRecognizer);
}
if (e.OldElement == null)
{
this.AddGestureRecognizer(swipeGestureRecognizer);
}
}
}
}
and the code that uses the MainPageStackLayout ...
<?xml version="1.0" encoding="utf-8" ?>
<ContentPage xmlns="http://xamarin.com/schemas/2014/forms"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
xmlns:local="clr-namespace:DriveLive"
x:Class="DriveLive.Views.MainPage">
<ListView x:Name="___drives" HasUnevenRows="True">
<ListView.ItemTemplate />
</ListView>
</ContentPage>
C#
public partial class MainPage : ContentPage
{
public MainPage()
{
InitializeComponent();
IDriveRespository repo = new DriveLive.Repository.Fakes.DriveRespository();
___drives.ItemsSource = repo.GetDrivesForUser(45); // returns a Drive list of objects
___drives.ItemTemplate = new DataTemplate(typeof(CustomViewCell));
___drives.SeparatorColor = Color.FromHex("#81C1B5");
}
}
public class CustomViewCell : ViewCell
{
bool _initialized = false;
StackLayout _cellStack;
public CustomViewCell()
{
_cellStack = new MainPageStackLayout()
{
Orientation = StackOrientation.Vertical,
HorizontalOptions = LayoutOptions.FillAndExpand
};
View = _cellStack;
var label = new Label() { FontAttributes = FontAttributes.Bold };
label.SetBinding(Label.TextProperty, new Binding("DestinationName"));
_cellStack.Children.Add(label);
}
}
From the handler for the UISwipeGestureRecognizer, how can I access the underlying Drive object which is databound to the ViewCell?

My issue is resolved by leveraging this piece of XForms.
ListView Interactivity - Context Actions
Credit goes to #skar's comment for pointing me in the right direction.

Related

Create bindable properties for Treeview in Xamarin Forms

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>

Xamarin IOS Custom Renderer overriden Draw method not called

I am trying to load a customized slider control in a listview (with accordeon behaviour). When the View loads all the listview elements are collapsed so the slider control visibility is false. I observed that the overriden Draw method within the ios renderer is not called while the control is not visible so I end up having the native control within my listview.
I have reproduced the issue in a separate project:
I have the IOS custom renderer:
public class CustomGradientSliderRenderer : SliderRenderer
{
public CGColor StartColor { get; set; }
public CGColor CenterColor { get; set; }
public CGColor EndColor { get; set; }
protected override void OnElementChanged(ElementChangedEventArgs<Slider> e)
{
if (Control == null)
{
var customSlider = e.NewElement as CustomGradientSlider;
StartColor = customSlider.StartColor.ToCGColor();
CenterColor = customSlider.CenterColor.ToCGColor();
EndColor = customSlider.EndColor.ToCGColor();
var slider = new SlideriOS
{
Continuous = true,
Height = (nfloat)customSlider.HeightRequest
};
SetNativeControl(slider);
}
base.OnElementChanged(e);
}
public override void Draw(CGRect rect)
{
base.Draw(rect);
if (Control != null)
{
Control.SetMinTrackImage(CreateGradientImage(rect.Size), UIControlState.Normal);
}
}
void OnControlValueChanged(object sender, EventArgs eventArgs)
{
((IElementController)Element).SetValueFromRenderer(Slider.ValueProperty, Control.Value);
}
public UIImage CreateGradientImage(CGSize rect)
{
var gradientLayer = new CAGradientLayer()
{
StartPoint = new CGPoint(0, 0.5),
EndPoint = new CGPoint(1, 0.5),
Colors = new CGColor[] { StartColor, CenterColor, EndColor },
Frame = new CGRect(0, 0, rect.Width, rect.Height),
CornerRadius = 5.0f
};
UIGraphics.BeginImageContext(gradientLayer.Frame.Size);
gradientLayer.RenderInContext(UIGraphics.GetCurrentContext());
var image = UIGraphics.GetImageFromCurrentImageContext();
UIGraphics.EndImageContext();
return image.CreateResizableImage(UIEdgeInsets.Zero);
}
}
public class SlideriOS : UISlider
{
public nfloat Height { get; set; }
public override CGRect TrackRectForBounds(CGRect forBounds)
{
var rect = base.TrackRectForBounds(forBounds);
return new CGRect(rect.X, rect.Y, rect.Width, Height);
}
}
The View with codebehind:
Main.xaml:
<?xml version="1.0" encoding="utf-8" ?>
<ContentPage
x:Class="GradientSlider.MainPage"
xmlns="http://xamarin.com/schemas/2014/forms"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
xmlns:local="clr-namespace:GradientSlider">
<ContentPage.Content>
<Grid>
<StackLayout x:Name="SliderContainer">
<local:CustomGradientSlider
x:Name="mySlider"
CenterColor="#feeb2f"
CornerRadius="16"
EndColor="#ba0f00"
HeightRequest="20"
HorizontalOptions="FillAndExpand"
Maximum="10"
Minimum="0"
StartColor="#6bab29"
VerticalOptions="CenterAndExpand"
MaximumTrackColor="Transparent"
ThumbColor="green"
/>
<Label x:Name="lblText" Text="txt"
VerticalOptions="Center" HorizontalOptions="Center"/>
</StackLayout>
<Button Text="Magic" Clicked="Button_Tapped" WidthRequest="100" HeightRequest="50" VerticalOptions="Center" HorizontalOptions="Center"/>
</Grid>
</ContentPage.Content>
</ContentPage>
Main.xaml.cs:
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using Xamarin.Forms;
namespace GradientSlider
{
public partial class MainPage : ContentPage, INotifyPropertyChanged
{
public MainPage()
{
InitializeComponent();
SliderContainer.IsVisible = false;
}
void Button_Tapped(object sender,ClickedEventArgs a)
{
SliderContainer.IsVisible = !SliderContainer.IsVisible;
}
}
}
So in the scenario above you can see that when I load the main.xaml the control is invisible (SliderContainer.IsVisible = false;) in this case I get a native slider control and not my custom one. If I change in the constructor SliderContainer.IsVisible = true; then I get my custom control.
After an investigation I realised that if the control is not visible when the view loads the public override void Draw(CGRect rect) is not called. I could not find any solution to trigger the Draw method while the control is invisible.
Anybody has an idea how to load a custom renderer correctly while the control is not visible ?
Thank you!
Assuming the renderer is overriding OnElementPropertyChanged:
protected override void OnElementChanged(ElementChangedEventArgs<MyFormsSlider> e)
{
if (e.NewElement != null)
{
if (Control == null)
{
// Instantiate the native control and assign it to the Control property with
// the SetNativeControl method
SetNativeControl(new MyNativeControl(...
...
}
protected override void OnElementPropertyChanged(object sender, PropertyChangedEventArgs e)
{
base.OnElementPropertyChanged(sender, e);
//assuming MyFormsSlider derives from View / VisualElement; the latter has IsVisibleProperty
if (e.PropertyName == MyFormsSlider.IsVisibleProperty.PropertyName)
{
//Control is the control set with SetNativeControl
Control. ...
}
...
}

How to create a Xamarin Tooltip in code-behind

I am testing using the following example. https://github.com/CrossGeeks/TooltipSample
The sample works fine, it even works with Labels (sample uses buttons, images and boxviews). The issue is in my main App I need to create the tooltips in code behind.
To test how to do it, in the very same solution (from that above example) I created a TestPage and made it my MainPage in App.xaml.cs. The XAML looks like this:
<?xml version="1.0" encoding="utf-8" ?>
<ContentPage
xmlns="http://xamarin.com/schemas/2014/forms"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
x:Class="ToolTipSample.TestPage">
<ContentPage.Content>
<StackLayout
x:Name="mainLayout"
BackgroundColor="Yellow">
<StackLayout.GestureRecognizers>
<TapGestureRecognizer Tapped="Handle_Tapped"/>
</StackLayout.GestureRecognizers>
</StackLayout>
</ContentPage.Content>
The code-behind looks like this:
using Xamarin.Forms;
using Xamarin.Forms.Xaml;
using ToolTipSample.Effects;
namespace ToolTipSample
{
[XamlCompilation(XamlCompilationOptions.Compile)]
public partial class TestPage : ContentPage
{
public TestPage()
{
InitializeComponent();
var actionLabel = new Label
{
Text = "Show Tooltip",
WidthRequest = 150,
VerticalOptions = LayoutOptions.StartAndExpand,
HorizontalOptions = LayoutOptions.Center,
BackgroundColor = Color.Wheat
};
// Add tooltip to action label
TooltipEffect.SetPosition(actionLabel, TooltipPosition.Bottom);
TooltipEffect.SetBackgroundColor(actionLabel, Color.Silver);
TooltipEffect.SetTextColor(actionLabel, Color.Teal);
TooltipEffect.SetText(actionLabel, "This is the tooltip");
TooltipEffect.SetHasTooltip(actionLabel, true);
actionLabel.Effects.Add(Effect.Resolve($"CrossGeeks.{nameof(TooltipEffect)}"));
mainLayout.Children.Add(actionLabel);
}
void Handle_Tapped(object sender, System.EventArgs e)
{
foreach (var c in mainLayout.Children)
{
if (TooltipEffect.GetHasTooltip(c))
{
TooltipEffect.SetHasTooltip(c, false);
TooltipEffect.SetHasTooltip(c, true);
}
}
}
}
}
All other code unchanged.
When I tap the label, the tooltip appears as expected. But when I tap the background it does not disappear (like those created in XAML in the sample).
One other thing. If I tap twice it disappears.
Can anyone see what I am missing?
Thanks.
According to your description and code, you can delete the following line code to achieve your requirement.
actionLabel.Effects.Add(Effect.Resolve($"CrossGeeks.{nameof(TooltipEffect)}"));
You don't need to add effect for control when page load, because this effect will be added when you click this control by these code:
static void OnHasTooltipChanged(BindableObject bindable, object oldValue, object newValue)
{
var view = bindable as View;
if (view == null)
{
return;
}
bool hasTooltip = (bool)newValue;
if (hasTooltip)
{
view.Effects.Add(new ControlTooltipEffect());
}
else
{
var toRemove = view.Effects.FirstOrDefault(e => e is ControlTooltipEffect);
if (toRemove != null)
{
view.Effects.Remove(toRemove);
}
}
}

Xamarin WebView scale to fit

Is there a way to set a WebView in Xamarin to scale its content to fit the screen by default and still allow for pinch zooming in/out?
We're going to use this to display documents we have online.
I solved the scale fit to page and zooming by Custom Renderers, as given below
For IOS
public class CustomWebViewRenderer : WebViewRenderer
{
protected override void OnElementChanged(VisualElementChangedEventArgs e)
{
base.OnElementChanged(e);
var view = Element as CustomWebView;
if (view == null || NativeView == null)
{
return;
}
this.ScalesPageToFit = true;
}
}
For Android
public class CustomWebViewRenderer : WebViewRenderer
{
protected override void OnElementChanged(ElementChangedEventArgs<WebView> e)
{
base.OnElementChanged(e);
if (Control != null)
{
Control.Settings.BuiltInZoomControls = true;
Control.Settings.DisplayZoomControls = false;
Control.Settings.LoadWithOverviewMode = true;
Control.Settings.UseWideViewPort = true;
}
}
}
If you're using Xamarin.Forms that would look something like this
<?xml version="1.0" encoding="UTF-8"?>
<ContentPage xmlns="http://xamarin.com/schemas/2014/forms"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
x:Class="WebViewDemo.LoadingDemo" Title="Loading Demo">
<ContentPage.Content>
<StackLayout>
<WebView x:Name="Browser"
VerticalOptions = LayoutOptions.FillAndExpand ,
HorizontalOptions = LayoutOptions.FillAndExpand,
</StackLayout>
</ContentPage.Content>
</ContentPage>
This should fill the page and not affect pinch and zoom.

System.NullReferenceException Xamarin Maps

I'm getting a NullException when will display the map, but can not detect;
I tried to change the name of the maps, locazation, etc., but without success.
You may be able to identify where is my mistake, please help me!
public partial class LocationPage : ContentPage
{
public Clinica _clinica;
public LocationPage(Clinica clinica)
{
InitializeComponent();
Clinica = clinica;
SetupMap();
}
public Clinica Clinica
{
get
{
return _clinica;
}
set
{
_clinica = value;
}
}
protected override void OnAppearing()
{
base.OnAppearing();
// Typically, is preferable to call into the viewmodel for OnAppearing() logic to be performed,
// but we're not doing that in this case because we need to interact with the Xamarin.Forms.Map property on this Page.
// In the future, the Map type and it's properties may get more binding support, so that the map setup can be omitted from code-behind.
SetupMap();
}
void SetupMap()
{
if (Device.OS != TargetPlatform.WinPhone && Device.OS != TargetPlatform.Windows)
{
var pin = new Pin()
{
Type = PinType.Place,
Position = new Position(Clinica.Latitude, Clinica.Longitude),
Label = Clinica.Nome
};
clinicaMap.Pins.Clear();
clinicaMap.Pins.Add(pin);
clinicaMap.MoveToRegion(MapSpan.FromCenterAndRadius(pin.Position, Distance.FromMiles(10)));
}
}
}
}
LocationPage.xaml
<?xml version="1.0" encoding="utf-8" ?>
<ContentPage xmlns="http://xamarin.com/schemas/2014/forms"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
xmlns:maps="clr-namespace:Xamarin.Forms.Maps;assembly=Xamarin.Forms.Maps"
x:Class="CartaoDeTodos.View.LocationPage">
<StackLayout>
<maps:Map x:Name="clinicaMap"
VerticalOptions="FillAndExpand"/>
</StackLayout>
</ContentPage>

Resources