Android9 Flexlayout bug? - xaml

I have a flexlayout inside of a frame, a grid and a stacklayout:
<CollectionView x:Name="collectionView"
Margin="20"
SelectionMode="Single"
SelectionChanged="OnSelectionChanged">
<CollectionView.ItemsLayout>
<LinearItemsLayout Orientation="Vertical"
ItemSpacing="10" />
</CollectionView.ItemsLayout>
<!-- Define the appearance of each item in the list -->
<CollectionView.ItemTemplate>
<DataTemplate>
<StackLayout Margin="0,10,0,0" Spacing="0">
<StackLayout Orientation="Horizontal" Margin="0">
<Label FontSize="Medium" FontAttributes="Bold" Text="{Binding Name, Mode=OneWay}" />
<Label Text="{Binding Bottle.ID, Mode=OneWay}" FontSize="Medium"
FontAttributes="Italic" VerticalTextAlignment="Center"
Margin="20,0,0,0"/>
</StackLayout>
<Frame CornerRadius="10" BorderColor="Black" IsVisible="{Binding IsConnected, Converter={StaticResource InverseBooleanConverter}, Mode=OneWay}" Padding="10">
<Label Text="not connected" FontSize="Small"/>
</Frame>
<Frame CornerRadius="10" BorderColor="Black" IsVisible="{Binding IsConnected, Mode=OneWay}" Padding="10">
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*" />
<ColumnDefinition Width="40" />
</Grid.ColumnDefinitions>
<StackLayout Grid.Column="0">
<FlexLayout BindableLayout.ItemsSource="{Binding Bottle.Components, Mode=OneWay}"
Direction="Row" JustifyContent="Start" Wrap="Wrap"
AlignItems="Center" AlignContent="Start"
>
<BindableLayout.ItemTemplate>
<DataTemplate>
<Label FontSize="Small" Text="{Binding DisplayString}" VerticalOptions="CenterAndExpand" Margin="0,0,10,0" Padding="0"/>
</DataTemplate>
</BindableLayout.ItemTemplate>
</FlexLayout>
<StackLayout Orientation="Horizontal" IsVisible="{Binding IsConnected, Mode=OneWay}" VerticalOptions="FillAndExpand">
<Label FontSize="Small" Text="in " IsVisible="{Binding Bottle.IsCarrier, Converter={StaticResource InverseBooleanConverter}, Mode=OneWay}"/>
<Label FontSize="Small" Text="{Binding Bottle.CarrierGas, Mode=OneWay}"/>
<Label FontSize="Small" Text=" 100%" IsVisible="{Binding Bottle.IsCarrier, Mode=OneWay}" />
</StackLayout>
</StackLayout>
<Label FontSize="Small" Text="(ISO)" FontAttributes="Italic" Grid.Column="1" IsVisible="False">
<Label.Triggers>
<MultiTrigger TargetType="Label">
<MultiTrigger.Conditions>
<BindingCondition Binding="{Binding Bottle.IsISOCertified, Mode=OneWay}" Value="True" />
<BindingCondition Binding="{Binding IsConnected, Mode=OneWay}" Value="True" />
</MultiTrigger.Conditions>
<Setter Property="IsVisible" Value="True" />
</MultiTrigger>
</Label.Triggers>
</Label>
</Grid>
</Frame>
</StackLayout>
</DataTemplate>
</CollectionView.ItemTemplate>
</CollectionView>
At a specific input data, the flexlayout is not displayed correctly (see first element in the list):
If I remove the frame, the texts are displayed correctly:
If I keep the frame and increase the margin or the padding of the flexlayout elements by one, the text is displayed correctly in 2 lines.
<DataTemplate>
<Label FontSize="Small" Text="{Binding DisplayString}" VerticalOptions="CenterAndExpand" Margin="0,0,11,0"/>
</DataTemplate>
Also if I increase the fontsize to medium, it is displayed correctly. Plus, if I do not restart the application but let visual studio automatically update the form, after decreasing the font size, the display is correct. However, if I restart the Android Emulator, the problem is there again.
With font size medium:
<DataTemplate>
<Label FontSize="Medium" Text="{Binding DisplayString}" VerticalOptions="CenterAndExpand" Margin="0,0,10,0"/>
</DataTemplate>
After resetting it to small, but not restarting the app:
I guess, increasing the Margin/Padding or increasing the font size or removing the frame would not work correctly on the long run, because there's always going to be some data/screen size combination which breaks the layout.
It seems to me, that the layout only fails in some corner cases. However, when the element widths (+margins) are over some limit, the flexlayout recognizes that the elements have to be displayed in 2 lines, and after that the layout is ok.
Is this a bug of xamarin/android, or is there a problem in my code? Could anyone suggest a workaround, solution, that works reliably?
Some extra information, not sure what is necessary:
Target Android Version: Android 9.0 (API Level 28 - Pie)
emulator: Pixel 2 Pie 9.0 - API28 1080x1920 420dpi
Edit: I also tested with 1020x1920, 1000x1920, 800x1920, 1200x1920 display size, all of them showed the same problem. So either the setting in Android Device Manager was not taken over (hw.lcd.width), or the problem is independent of the display size.
Edit2: changing the display size in Settings/Display affects the layout, and fixes it, if I set it to lower or larger. But I guess it is only changing the FontSize parameter of the labels.
While testing #ToolmakerSteve's solution, I found out, that the children bound.Width-s are higher if I remove the last item in the list (NO 2100ppm). Then I called measure for each children, and I saw, that the widths match those bound.Width-s when the last item was missing. So I ended up with this code:
//https://stackoverflow.com/a/70526712/9963147
class FlexLayoutImproveMeasure : FlexLayout
{
protected override void LayoutChildren(double x, double y, double width, double height)
{
base.LayoutChildren(x, y, width, height);
foreach (var child in Children)
{
Rectangle cb = child.Bounds;
Size s = child.Measure(double.PositiveInfinity, cb.Height).Request;
double newW = s.Width;
var childBounds2 = new Rectangle(cb.X, cb.Y, newW, cb.Height);
child.Layout(childBounds2);
}
}
}

I discovered that FlexLayout sometimes doesn't give each label quite enough room; the labels get truncated.
Here is a subclass of FlexLayout, that adds a small amount of width to each Label.
FlexLayoutImproveMeasure.cs:
using Xamarin.Forms;
namespace XFSOAnswers
{
class FlexLayoutImproveMeasure : FlexLayout
{
protected override void LayoutChildren(double x, double y, double width, double height)
{
base.LayoutChildren(x, y, width, height);
//var childBoundss = new List<Rectangle>();
foreach (var child in Children)
{
Rectangle cb = child.Bounds;
// Make each child slightly wider: labels were being truncated.
// Less than 2 didn't always work. Increase as needed, up to right margin.
const int extraW = 2;
double newX = cb.X;
// TBD whether part of the problem is label that isn't pixel-aligned.
// (So far, seems more reliable to increase extraW instead.)
//MAYBE double newX = Math.Floor(cb.X);
double newW = cb.Width + extraW;
// TBD whether it helps to ensure width extends to next pixel.
// (So far, seems more reliable to increase extraW instead.)
//MAYBE double newW = Math.Ceiling(cb.Width) + extraW;
//double ceilingRight = Math.Ceiling(cb.X + cb.Width);
//MAYBE double newW = ceilingRight - Math.Floor(cb.X) + extraW;
var childBounds2 = new Rectangle(newX, cb.Y, newW, cb.Height);
child.Layout(childBounds2);
//childBoundss.Add(child.Bounds);
//childBoundss.Add(cb);
}
}
}
}
Use this in place of FlexLayout. This is done by adding to a page an xmlns: declaration to access a namespace in your project. Here the namespace is XFSOAnswers. Change xmlns:local line to refer to whatever namespace you put the above class in.
XAML of a page:
<?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:XFSOAnswers"
x:Class="XFSOAnswers.FlexLayoutInItemTemplatePage">
<ContentPage.Content>
<StackLayout>
<!-- labels truncated at width in range 188-194 -->
<CollectionView ItemsSource="{Binding Items}"
WidthRequest="190" HorizontalOptions="Center"
Margin="20" >
<CollectionView.ItemsLayout>
<LinearItemsLayout Orientation="Vertical" ItemSpacing="10" />
</CollectionView.ItemsLayout>
<CollectionView.ItemTemplate>
<DataTemplate>
<Frame CornerRadius="10" BorderColor="Black"
BackgroundColor="LightBlue" Padding="10">
<local:FlexLayoutImproveMeasure
Wrap="Wrap"
BindableLayout.ItemsSource="{Binding Items2}"
BackgroundColor="LightGray" >
<BindableLayout.ItemTemplate>
<DataTemplate>
<Label FontSize="Small" Text="{Binding .}"
VerticalOptions="Start" HorizontalOptions="Start" Margin="0,0,10,0" Padding="0"/>
</DataTemplate>
</BindableLayout.ItemTemplate>
</local:FlexLayoutImproveMeasure>
</Frame>
</DataTemplate>
</CollectionView.ItemTemplate>
</CollectionView>
</StackLayout>
</ContentPage.Content>
</ContentPage>
page's code behind has the bindings:
using System.Collections.Generic;
using Xamarin.Forms;
using Xamarin.Forms.Xaml;
namespace XFSOAnswers
{
[XamlCompilation(XamlCompilationOptions.Compile)]
public partial class FlexLayoutInItemTemplatePage : ContentPage
{
public FlexLayoutInItemTemplatePage()
{
InitializeComponent();
BindingContext = this;
}
public List<ListModel> Items { get; } = new List<ListModel> {
new ListModel(),
new ListModel(),
};
}
public class ListModel
{
public List<string> Items2 { get; } = new List<string> {
"aaa", "bbbbbbbbb", "cc", "dddd", "e",
};
}
}
Items2 strings chosen such that at WidthRequest=190, "dddd" barely fit on first line. Tests were done with and without the final "e", to see what FlexLayout did in both cases.
Original FlexLayout, truncating labels:
Using FlexLayoutImproveMeasure with extraW = 2:

Related

Get (and bind to) current displaywidth of element in Maui

In my xaml setup I have a Label, which should wrap its text. It is contained within a border, and I want the Label width to be the width of the border. I've tried to do this with the following binding on the label:
WidthRequest="{Binding Source={RelativeSource Mode=FindAncestor, AncestorType={x:Type Border}}, Path=Width}"
This has not worked though, and i suspect it hasn't because the Width that gets returned is NaN (I've written out the width of the border during runtime and it was NaN). But this is confusing to me, as I have a binding on the Border to the width of the Flexlayout which is working. Is there anything I'm missing and is there any better way to do this?
xaml:
<ScrollView Grid.Row="1">
<FlexLayout BindableLayout.ItemsSource="{Binding DisplayedUserActions}" JustifyContent="Start" Wrap="Wrap" Direction="Row">
<BindableLayout.ItemTemplate>
<DataTemplate>
<Border FlexLayout.AlignSelf="Stretch" FlexLayout.Basis="{Binding Source={RelativeSource FindAncestor, AncestorType{x:Type ScrollView}}, Path=Width, Converter={StaticResource WidthToFlexlayoutBasisConverter}}">
<Grid RowDefinitions="*,*">
-- more content --
<Label Grid.Row="1" Text="{Binding Title}" LineBreakMode="WordWrap" HorizontalTextAlignment="Center" VerticalOptions="Center" HorizontalOptions="Center" TextColor="Black" Margin="5" WidthRequest="{Binding Source={RelativeSource Mode=FindAncestor, AncestorType={x:Type Border}}, Path=Width}"/>
</Grid>
</Border>
</DataTemplate>
</BindableLayout.ItemTemplate>
</FlexLayout>
</ScrollView>
You can use data binding to achieve the same width for Label and Border. Bind the Border and Label's WidthRequest properties to the TestWidth property in the same binding context. like this:
Xaml:
<StackLayout>
<StackLayout.BindingContext>
<local:MyViewModel/>
</StackLayout.BindingContext>
<Border BackgroundColor="Pink" WidthRequest="{Binding Testwidth}">
<Label x:Name="lable" Text="test1111111111111111" WidthRequest="{Binding Testwidth}"/>
</Border>
</StackLayout>
ViewModel:
public class MyViewModel : INotifyPropertyChanged
{
public double testwidth;
public event PropertyChangedEventHandler PropertyChanged;
public double Testwidth
{
get { return testwidth; }
set
{
if (testwidth != value)
{
testwidth = value;
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs("Testwidth"));
}
}
}
public MyViewModel() { testwidth= 100; }
}

Resume and modify some graphic elements present in another page

I have created a ControlTemplate in the app.xaml, defining the graphic part, only in the cs I would like to take some elements, to which I have given the name, to assign them the click event, but it signals me that they do not exist in the current context .
Someone who could kindly tell me how to please?
Someone, if possible, modify the source of the webview present in the main page, directly from app.xaml.cs
My code in the template is this:`
<Application.Resources>
<ResourceDictionary>
<ControlTemplate x:Key="HeaderFooterTemplate"><ContentPresenter />
<StackLayout>
<BoxView HorizontalOptions="FillAndExpand" BackgroundColor="#DDDDDD" HeightRequest="2" Margin="0" />
<ScrollView Orientation="Horizontal">
<Grid RowSpacing="0">
<StackLayout Background="#c9ced6" HeightRequest="120" MinimumHeightRequest="120" Orientation="Horizontal" Grid.Row="0" Spacing="2">
<Grid WidthRequest="100" MinimumHeightRequest="120" BackgroundColor="#373B53" Padding="10" x:Name="Dashboard">
<Image Source="homeArancione" x:Name="imgDashboard" HorizontalOptions="Center" VerticalOptions="Center" WidthRequest="30" HeightRequest="30" Grid.Row="0" />
<Label Text="DASHBOARD" TextColor="#c9ced6" FontAttributes="Bold" HorizontalOptions="Center" Grid.Row="1" />
</Grid></Grid>
And in the reference page I recall it like this:
ControlTemplate="{StaticResource HeaderFooterTemplate}"
Graphically it works, but in the cs is I can't modify the texts to the labels or assign click events to the elements because it tells me that they don't exist in the current context
If you want to change the text or trigger the click event of the label in ControlTemplate, you could use the GestureRecognizers. The x:Name could not be found in the ControlTemplate directly.
<ControlTemplate x:Key="HeaderFooterTemplate">
<!--<ContentPresenter />-->
<StackLayout>
<BoxView HorizontalOptions="FillAndExpand" BackgroundColor="#DDDDDD" HeightRequest="2" Margin="0" />
<ScrollView Orientation="Horizontal">
<Grid RowSpacing="0">
<StackLayout Background="#c9ced6" HeightRequest="120" MinimumHeightRequest="120" Orientation="Horizontal" Grid.Row="0" Spacing="2">
<Grid WidthRequest="100" MinimumHeightRequest="120" BackgroundColor="#373B53" Padding="10" x:Name="Dashboard">
<Image Source="homeArancione" x:Name="imgDashboard" HorizontalOptions="Center" VerticalOptions="Center" WidthRequest="30" HeightRequest="30" Grid.Row="0" />
<Label Text="DASHBOARD" TextColor="#c9ced6" FontAttributes="Bold" HorizontalOptions="Center" Grid.Row="1">
<Label.GestureRecognizers>
<TapGestureRecognizer Tapped="TapGestureRecognizer_Tapped"></TapGestureRecognizer>
</Label.GestureRecognizers>
</Label>
</Grid>
</StackLayout>
</Grid>
</ScrollView>
</StackLayout>
</ControlTemplate>
Code behind: App.xaml.cs
private void TapGestureRecognizer_Tapped(object sender, EventArgs e)
{
Console.WriteLine("-----------");
var label = sender as Label;
label.Text = "Hello";
}
Update:
update the text of the label as soon as the page loads, without
having to click
Xamarin provide GetTemplateChild to get a named element from a template. The GetTemplateChild method should only be called after the OnApplyTemplate method has been called. If you want to call the OnApplyTemplate method, this template should be added for the contentpage.
You could refer to the thread i done before.
Xamarin SwipeView Open Left Item In ControlTemplate

Scrolling when there are two CollectionViews in ContentPage

I have two CollectionViews in my ContentPage inside a StackLayout, one above the other. Each binds to a separate ItemsSource. Above each one I have a Label. At this point each one take up 50% of the screen and scrolls separately.
I would like everything to scroll as though it were one long list.
So I surrounded everything with a ScrollView. But then, depending on where you put your finger, the scroll may scroll the entire page (which is what I want) or just the current CollectionView.
It seems like there is no way to cancel the scroll capability of the CollectionView. Is that true? and if not, How should I set up my ContentPage ?
In the below example both CollectionViews have the same model and binding but in reality they will be different.
Here is the xaml:
<RefreshView
x:DataType="local:AllRestaurantsViewModel"
Command="{Binding LoadItemsCommand}"
IsRefreshing="{Binding IsBusy, Mode=TwoWay}">
<ScrollView>
<StackLayout>
<Label
FontSize="Large"
HorizontalOptions="Center"
Text="Suggested Restaurants" />
<CollectionView
x:Name="ItemsListView"
ItemsSource="{Binding SuggestedRestsComments}"
SelectionMode="None">
<CollectionView.ItemTemplate>
<DataTemplate>
<StackLayout Padding="10" x:DataType="model:SuggestedRestsComment">
<Label
FontSize="16"
LineBreakMode="NoWrap"
Style="{DynamicResource ListItemTextStyle}"
Text="{Binding restaurantName}" />
<Label
FontSize="13"
LineBreakMode="NoWrap"
Style="{DynamicResource ListItemDetailTextStyle}"
Text="{Binding CityName}" />
<StackLayout.GestureRecognizers>
<TapGestureRecognizer
Command="{Binding Source={RelativeSource AncestorType={x:Type local:ItemsViewModel}}, Path=ItemTapped}"
CommandParameter="{Binding .}"
NumberOfTapsRequired="1" />
</StackLayout.GestureRecognizers>
</StackLayout>
</DataTemplate>
</CollectionView.ItemTemplate>
</CollectionView>
<Label
FontSize="Large"
HorizontalOptions="Center"
Text="Existing Restaurants" />
<CollectionView
x:Name="ItemsListView2"
ItemsSource="{Binding SuggestedRestsComments}"
SelectionMode="None">
<CollectionView.ItemTemplate>
<DataTemplate>
<StackLayout Padding="10" x:DataType="model:SuggestedRestsComment">
<Label
FontSize="16"
LineBreakMode="NoWrap"
Style="{DynamicResource ListItemTextStyle}"
Text="{Binding restaurantName}" />
<Label
FontSize="13"
LineBreakMode="NoWrap"
Style="{DynamicResource ListItemDetailTextStyle}"
Text="{Binding CityName}" />
<StackLayout.GestureRecognizers>
<TapGestureRecognizer
Command="{Binding Source={RelativeSource AncestorType={x:Type local:ItemsViewModel}}, Path=ItemTapped}"
CommandParameter="{Binding .}"
NumberOfTapsRequired="1" />
</StackLayout.GestureRecognizers>
</StackLayout>
</DataTemplate>
</CollectionView.ItemTemplate>
</CollectionView>
</StackLayout>
</ScrollView>
</RefreshView>
You could have a try with Custom CollectionViewRenderer to achieve that in each platform.
For example, send a mesage in Forms:
void OnButtonClicked(object sender, EventArgs e)
{
MessagingCenter.Send<object>(this, "StopScrollinng");
}
Then in iOS CustomCollectionViewRenderer class stop scrolling:
public class CustomCollectionViewRenderer: CollectionViewRenderer
{
protected override void OnElementChanged(ElementChangedEventArgs<GroupableItemsView> e)
{
base.OnElementChanged(e);
MessagingCenter.Subscribe<object>(this, "StopScrollinng", (sender) =>
{
// Do something whenever the "StopScrollinng" message is received
if (Control != null)
{
NSArray s = Control.ValueForKey(new NSString("_subviewCache")) as NSMutableArray;
UICollectionView c = s.GetItem<UICollectionView>(0);
c.SetContentOffset(c.ContentOffset, true);
}
});
}
}
And in Android CustomCollectionViewRenderer class stop scrolling:
public class CustomCollectionViewRenderer: CollectionViewRenderer
{
public CustomCollectionViewRenderer(Context context) : base(context)
{
}
protected override void OnElementChanged(ElementChangedEventArgs<ItemsView> elementChangedEvent)
{
base.OnElementChanged(elementChangedEvent);
MessagingCenter.Subscribe<object>(this, "StopScrollinng", (sender) =>
{
// Do something whenever the "StopScrollinng" message is received
this.DispatchTouchEvent(MotionEvent.Obtain(SystemClock.UptimeMillis(), SystemClock.UptimeMillis(), MotionEventActions.Cancel, 0, 0, 0));
});
}
}
How about this?
In your scrollview set InputTransparent="True" this allows the input to go through to the layer underneath.
<ScrollView InputTransparent="True">
Then leave some white space (background) on the right side of the collection views.
Now when someone swipes in the white space, the entire page scrolls. And when someone swipes inside the collection view, the collection view scrolls.
taken from https://learn.microsoft.com/en-us/xamarin/xamarin-forms/user-interface/collectionview/populate-data
the idea is to not use two separate collectionviews and merge them into one by choosing a datatemplate at runtime
xmlns:controls="clr-namespace:<your namespace>.Controls"
<ContentPage.Resources>
<DataTemplate x:Key="DataTemplate1">
...
</DataTemplate>
<DataTemplate x:Key="DataTemplate2">
...
</DataTemplate>
<controls:DataTemplateSelector1 x:Key="DataTemplateSelector1"
Template1="{StaticResource DataTemplate1}"
Template2="{StaticResource DataTemplate2}" />
</ContentPage.Resources>
namespace <your namespace>.Controls
{
public class DataTemplateSelector1: DataTemplateSelector
{
public DataTemplate Template1 { get; set; }
public DataTemplate Template2 { get; set; }
protected override DataTemplate OnSelectTemplate(object item, BindableObject container)
{
//here you return which template you want to use based on the properties of "item" . e.g you can do item is SomeClass ? Template1 : Template2
}
}
}
<ScrollView>
<CollectionView x:Name="collection" ItemTemplate="{StaticResource DataTemplateSelector1}"></CollectionView>
</ScrollView>

How bind a command in DataTemplate in Resource Dictionary?

I'm trying to make a better solution architecture, for that I've separated many parts of code in differents files. Because my application use a lot of DataTemplates, I push them in different ResourceDictionary.xaml files.
Problem :
I have a view Agenda.xaml, with the viewModel AgendaViewModel. This view have a ListView which call's datatemplate in external ResourceDictionary file. But if I want put a Binding Command in the dataTemplate, the command is never executed because (I guess) the resource Dictionary where is my DataTemplate not reference ViewModel.
What can I do ?
I've already tried some weird Binding code like
<TapGestureRecognizer Command="{Binding BindingContext.OpenActiviteCommand, Source={x:Reference agendaPage}}" CommandParameter="{Binding .}"/>
Where "agendaPage" is the x:Name of Agenda.xaml.
All I found on Google was about WPF and Binding property not available on Xamarin Forms (RelativeSource, ElementName etc...)
I know I can put dataTemplate in my Agenda.xaml view, but I really want keep it in an external file. I want avoid view files with 1500 lines....
This is my Agenda.xaml view
<?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="Corim.Portable.CorimTouch.ViewForms.Agenda.AgendaViewDetail"
xmlns:converters="clr-namespace:Corim.Portable.CorimTouch.Converters"
Title="Agenda"
x:Name="agendaPage">
<ContentPage.Content>
<Grid HorizontalOptions="FillAndExpand" VerticalOptions="FillAndExpand" BackgroundColor="{StaticResource LightGrayCorim}">
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
</Grid.RowDefinitions>
<!-- Liste itv,pointage,activite -->
<ListView
x:Name="listAgenda"
Grid.Row="1"
SeparatorVisibility="None"
HasUnevenRows="True"
SelectionMode="None"
CachingStrategy="RecycleElement"
ItemsSource="{Binding AgendaList}"
ItemTemplate="{StaticResource agendaTemplateSelector}"
BackgroundColor="{StaticResource LightGrayCorim}">
</ListView>
</Grid>
</ContentPage.Content>
</ContentPage>
And this is one part of Datatemplate in AgendaTemplates.xaml
<DataTemplate x:Key="agenda-adresse-intervention">
<ViewCell>
<Frame Margin="10,5,10,0" HasShadow="False" Padding="0" CornerRadius="10" IsClippedToBounds="True">
<controls:CustomTappedStackLayout
BackgroundColor="White"
TappedBackgroundColor="{StaticResource RollOver}"
HorizontalOptions="FillAndExpand"
Orientation="Horizontal"
Padding="10">
<StackLayout.GestureRecognizers>
<TapGestureRecognizer Command="{Binding Path=BindingContext.OpenParcCommand, Source={x:Reference agendaPage}}" CommandParameter="{Binding .}" NumberOfTapsRequired="1"/>
</StackLayout.GestureRecognizers>
<Image
Source="localisation_adresse"
WidthRequest="30"
HeightRequest="30"
Aspect="AspectFit"
HorizontalOptions="Start"
Margin="10"
VerticalOptions="StartAndExpand"/>
<StackLayout
HorizontalOptions="FillAndExpand"
Orientation="Vertical">
<Label
Text="{Binding Client}"
IsVisible="{Binding Client, Converter={StaticResource StringEmptyBooleanConverter}}"
FontFamily="{StaticResource SemiBoldFont}"
FontSize="{StaticResource MediumTextSize}"
TextColor="Black"/>
<Label
Text="{Binding Title}"
IsVisible="{Binding Title, Converter={StaticResource StringEmptyBooleanConverter}}"
FontFamily="{StaticResource RegularFont}"
FontSize="{StaticResource DefaultTextSize}"
TextColor="Gray"/>
</StackLayout>
</controls:CustomTappedStackLayout>
</Frame>
</ViewCell>
</DataTemplate>
But if I want put a Binding Command in the dataTemplate, the command
is never executed because (I guess) the resource Dictionary where is
my DataTemplate not reference ViewModel.
You guess wrong: it's totally fine to do what you are doing and should work transparently. The binding is resolved at runtime your data template does not know anything about the object that will be bound.
1st: drop the BindingContext.OpenActiviteCommand nonsense :) Just bind to OpenActiviteCommand, the only question is:
2nd: Where is your OpenActiviteCommand ?
The data context of your AgendaTemplates is the item in your AgendaList.
If the type of the AgendaList is an ObservableCollection<AgendaViewModel>, and your AgendaViewModel has a OpenParcCommand then it should be fine:
public class AgendaViewModel
{
public AgendaViewModel(ICommand openParcCommand)
{
OpenParcCommand = openParcCommand;
}
public ICommand OpenParcCommand { get; }
}
and in your AgendaPageViewModel:
public class AgendaPageViewModel
{
public ObservableCollection<AgendaViewModel> AgendaList { get; }
}
Thanks to #Roubachof
The soluce was replace my ListView of InterventionModel by ListView of AgendaDataViewModel.
AgendaViewModel is a new class which contains all the commands I need, and an InterventionModel.
this is AgendaDataViewModel :
public class AgendaDataViewModel : HybridContentViewModel
{
private InterventionModel _model;
public InterventionModel Model
{
get => _model;
set { _model = value; }
}
public ICommand OpenActiviteCommand { get; private set; }
public AgendaDataViewModel()
{
this.OpenActiviteCommand = new Command<InterventionModel>(this.OpenActivite);
}
/// <summary>
/// Ouvre le formulaire d'édition de l'activité
/// </summary>
/// <param name="model"></param>
private void OpenActivite(InterventionModel model)
{
//TODO amener sur le formulaire d'activité
}
}
my AgendaTemplate.xaml
<!--Template pour l'affichage du parc-->
<DataTemplate x:Key="agenda-adresse-intervention">
<ViewCell>
<Frame Margin="10,5,10,0" HasShadow="False" Padding="0" CornerRadius="10" IsClippedToBounds="True">
<controls:CustomTappedStackLayout
BackgroundColor="White"
TappedBackgroundColor="{StaticResource RollOver}"
HorizontalOptions="FillAndExpand"
Orientation="Horizontal"
Padding="10">
<StackLayout.GestureRecognizers>
<TapGestureRecognizer Command="{Binding OpenParcCommand}" CommandParameter="{Binding Model}" NumberOfTapsRequired="1"/>
</StackLayout.GestureRecognizers>
<Image
Source="localisation_adresse"
WidthRequest="30"
HeightRequest="30"
Aspect="AspectFit"
HorizontalOptions="Start"
Margin="10"
VerticalOptions="StartAndExpand"/>
<StackLayout
HorizontalOptions="FillAndExpand"
Orientation="Vertical">
<Label
Text="{Binding Model.Client}"
IsVisible="{Binding Model.Client, Converter={StaticResource StringEmptyBooleanConverter}}"
FontFamily="{StaticResource SemiBoldFont}"
FontSize="{StaticResource MediumTextSize}"
TextColor="Black"/>
<Label
Text="{Binding Model.Title}"
IsVisible="{Binding Model.Title, Converter={StaticResource StringEmptyBooleanConverter}}"
FontFamily="{StaticResource RegularFont}"
FontSize="{StaticResource DefaultTextSize}"
TextColor="Gray"/>
</StackLayout>
</controls:CustomTappedStackLayout>
</Frame>
</ViewCell>
</DataTemplate>
As you can see, the values binding is made by this line :
{Binding Model.Client}
where Client is the name of Binded property. And to Bind a Command, you don't need Model, and just bind like this :
Command={Binding CommandName}
Hope it helps someone in the future !

How to make 3 column Xamarin.Forms FlexLayout with expanding center column and variable width left/right columns?

I'm trying to create a FlexLayout in Xamarin.Forms that will allow me to have the left and right columns be a variable width, and have the center column (and its contents) fill the remaining space and be centered on the screen.
Here is my current code, and here is what it's producing. Notice that "CENTER TEXT" in blue is centered within its StackLayout, but the StackLayout is not centered on the screen since the left and right columns have different widths.
Is FlexLayout a good choice for this, or should I use Grid or something else? Ideally, each column will expand to fit its content, with the center column's content being centered on the screen.
Note that the contents of each column is dynamic, so the widths of the left and right columns is also dynamic.
Thank you!
Code:
<FlexLayout x:Name="titleBar"
MinimumHeightRequest="40"
Padding="10"
JustifyContent="SpaceBetween"
AlignItems="Center"
AlignContent="Center">
<StackLayout x:Name="leftActionButton"
VerticalOptions="Center"
BackgroundColor="Red"
Orientation="Horizontal">
<Image x:Name="leftActionImg"
Margin="0, 0, 5, 0"
HeightRequest="40"
VerticalOptions="Center" />
<Label x:Name="leftActionLabel"
VerticalOptions="Center" />
</StackLayout>
<StackLayout VerticalOptions="Center"
BackgroundColor="Blue"
FlexLayout.Grow="1"
FlexLayout.Shrink="0">
<Label x:Name="title"
HorizontalTextAlignment="Center"/>
</StackLayout>
<StackLayout x:Name="rightActionButton"
BackgroundColor="Yellow"
VerticalOptions="Center"
Orientation="Horizontal">
<Label x:Name="rightActionLabel"
VerticalOptions="Center"
HorizontalOptions="End"
HorizontalTextAlignment="End" />
<Image x:Name="rightActionImg"
HeightRequest="40"
VerticalOptions="Center"
HorizontalOptions="End" />
</StackLayout>
</FlexLayout>
Results:
I was struggling with the same problem recently. So I was investing a day to find a solution. The result is disillusioning and I wouldn't call it a proper solution. I'm posting my thoughts here, because I couldn't find anything similar on the net.
I created a component which is responsible for balancing all three columns: It subscribes to width changes of the left and right column and propagates the relative difference to the center column.
The center column takes the relative difference as correction by applying a padding.
MainPage: In the Resources part you see that I'm declaring an instance of ColumnBalancer. Later I subscribe Width changes to ColumnBalancer, so ColumnBalancer gets to known when the very left and very right column (content, actually) changes. The PaddingOffset binding is retrieved from the same ColumnBalancer instance whenever the Width values have changed.
<ContentPage
x:Class="App7863.MainPage"
xmlns="http://xamarin.com/schemas/2014/forms"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
xmlns:d="http://xamarin.com/schemas/2014/forms/design"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:cb="clr-namespace:ColumnBalancing"
mc:Ignorable="d">
<ContentPage.Resources>
<ResourceDictionary>
<cb:ColumnBalancer x:Key="ColumnBalancer" x:Name="ColumnBalancer" />
</ResourceDictionary>
</ContentPage.Resources>
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
<RowDefinition Height="*" />
</Grid.RowDefinitions>
<!-- Header -->
<Grid
Grid.Row="0"
Padding="10"
BackgroundColor="LightGray">
<Grid.RowDefinitions>
<RowDefinition Height="39" />
<RowDefinition Height="Auto" />
</Grid.RowDefinitions>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto" />
<ColumnDefinition Width="*" />
<ColumnDefinition Width="Auto" />
</Grid.ColumnDefinitions>
<!-- Navigate Back Button -->
<Label
Grid.Column="0"
Width="{Binding Source={x:Reference ColumnBalancer}, Path=ReferenceWidthLeft}"
Text="<"
FontSize="20"
BackgroundColor="Magenta"
WidthRequest="39"
HeightRequest="39"
VerticalOptions="CenterAndExpand"
VerticalTextAlignment="Center"
HorizontalOptions="CenterAndExpand"
HorizontalTextAlignment="Center" />
<!-- Page Title -->
<StackLayout
Grid.Column="1"
Margin="0"
Padding="{Binding Source={x:Reference ColumnBalancer}, Path=PaddingOffset}"
BackgroundColor="LightYellow"
Spacing="0">
<Label
Text="Center Title"
FontSize="20"
BackgroundColor="Magenta"
HeightRequest="39"
VerticalOptions="CenterAndExpand"
VerticalTextAlignment="Center"
HorizontalOptions="CenterAndExpand"
HorizontalTextAlignment="Center"
LineBreakMode="TailTruncation" />
</StackLayout>
<!-- Toolbar Items -->
<StackLayout
Grid.Column="2"
Width="{Binding Source={x:Reference ColumnBalancer}, Path=ReferenceWidthRight}"
Margin="0"
Padding="0"
BackgroundColor="LightGreen"
Orientation="Horizontal"
Spacing="6">
<Label
x:Name="ToolbarT1"
Text="T1"
FontSize="20"
BackgroundColor="Green"
WidthRequest="39"
HeightRequest="39"
VerticalOptions="CenterAndExpand"
VerticalTextAlignment="Center"
HorizontalOptions="CenterAndExpand"
HorizontalTextAlignment="Center"
IsVisible="False" />
<Label
x:Name="ToolbarT2"
Text="T2"
FontSize="20"
BackgroundColor="Green"
WidthRequest="39"
HeightRequest="39"
VerticalOptions="CenterAndExpand"
VerticalTextAlignment="Center"
HorizontalOptions="CenterAndExpand"
HorizontalTextAlignment="Center"
IsVisible="False" />
</StackLayout>
<Label
Grid.Row="2"
Grid.ColumnSpan="3"
Text="Subtitle with more info"
FontSize="20"
BackgroundColor="LightBlue"
HeightRequest="39"
VerticalOptions="CenterAndExpand"
VerticalTextAlignment="Center"
HorizontalOptions="StartAndExpand"
HorizontalTextAlignment="Start" />
</Grid>
<!-- Content -->
<Grid
Grid.Row="1"
Padding="40"
BackgroundColor="LightCoral">
<StackLayout BackgroundColor="LightBlue">
<Button Text="Toogle T1" Clicked="Button_ToogleT1" />
<Button Text="Toogle T2" Clicked="Button_ToogleT2" />
</StackLayout>
</Grid>
</Grid>
</ContentPage>
ColumnBalancer: Exposes a ReferenceWidthLeft and ReferenceWidthRight which take the Width values from the left resp. right column content. Whenever the ReferenceWidth* properties change, a new PaddingOffset is set.
public class ColumnBalancer : BindableObject
{
public static readonly BindableProperty PaddingOffsetProperty = BindableProperty.Create(
nameof(PaddingOffset),
typeof(Thickness),
typeof(ColumnBalancer),
default(Thickness),
BindingMode.OneWay);
public Thickness PaddingOffset
{
get => (Thickness)this.GetValue(PaddingOffsetProperty);
set => this.SetValue(PaddingOffsetProperty, value);
}
public static readonly BindableProperty ReferenceWidthRightProperty = BindableProperty.Create(
nameof(ReferenceWidthRight),
typeof(double),
typeof(ColumnBalancer),
default(double),
BindingMode.OneWayToSource,
null,
OnReferenceWidthRightPropertyChanged);
public double ReferenceWidthRight
{
get => (double)this.GetValue(ReferenceWidthRightProperty);
set => this.SetValue(ReferenceWidthRightProperty, value);
}
public static readonly BindableProperty ReferenceWidthLeftProperty = BindableProperty.Create(
nameof(ReferenceWidthLeft),
typeof(double),
typeof(ColumnBalancer),
default(double),
BindingMode.OneWayToSource,
null,
OnReferenceWidthLeftPropertyChanged);
public double ReferenceWidthLeft
{
get => (double)this.GetValue(ReferenceWidthLeftProperty);
set => this.SetValue(ReferenceWidthLeftProperty, value);
}
private static void OnReferenceWidthLeftPropertyChanged(BindableObject bindable, object oldvalue, object newvalue)
{
if (!(bindable is ColumnBalancer columnBalancer) || !(newvalue is double newLeftValue && newLeftValue >= 0))
{
return;
}
UpdatePaddingOffset(columnBalancer, newLeftValue, columnBalancer.ReferenceWidthRight);
}
private static void OnReferenceWidthRightPropertyChanged(BindableObject bindable, object oldvalue, object newvalue)
{
if (!(bindable is ColumnBalancer columnBalancer) || !(newvalue is double newRightValue && newRightValue >= 0))
{
return;
}
UpdatePaddingOffset(columnBalancer, columnBalancer.ReferenceWidthLeft, newRightValue);
}
private static void UpdatePaddingOffset(ColumnBalancer columnBalancer, double left, double right)
{
if (left < 0)
{
left = 0;
}
if (right < 0)
{
right = 0;
}
var relativePadding = Math.Abs(left - right);
if (right > left)
{
columnBalancer.PaddingOffset = new Thickness(relativePadding, 0, 0, 0);
}
else
{
columnBalancer.PaddingOffset = new Thickness(0, 0, relativePadding, 0);
}
}
}
I was able to get this to work by using a 2-column grid and overlaying the "center" column on top by using Grid.ColumnSpan="2".
I realize this has the potential for the content in the center to overlap the content on the left and right, but I'm OK with working around this limitation if that's the best I can do. Still welcome other, more robust suggestions, though!
<Grid x:Name="titleBar"
MinimumHeightRequest="40"
Padding="10"
HorizontalOptions="FillAndExpand">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*" />
<ColumnDefinition Width="*" />
</Grid.ColumnDefinitions>
<StackLayout x:Name="leftActionButton"
VerticalOptions="Center"
Orientation="Horizontal"
HorizontalOptions="Start"
Grid.Column="0">
<Image x:Name="leftActionImg"
Margin="0, 0, 5, 0"
HeightRequest="40"
VerticalOptions="Center" />
<Label x:Name="leftActionLabel"
VerticalOptions="Center" />
</StackLayout>
<StackLayout x:Name="rightActionButton"
VerticalOptions="Center"
HorizontalOptions="End"
Orientation="Horizontal"
Grid.Column="1">
<Label x:Name="rightActionLabel"
VerticalOptions="Center"
HorizontalOptions="End"
HorizontalTextAlignment="End" />
<Image x:Name="rightActionImg"
HeightRequest="40"
VerticalOptions="Center"
HorizontalOptions="End" />
</StackLayout>
<StackLayout VerticalOptions="Center"
HorizontalOptions="CenterAndExpand"
Grid.Column="0"
Grid.ColumnSpan="2">
<Label x:Name="title"
HorizontalTextAlignment="Center"/>
</StackLayout>
</Grid>