Xamarin Forms - Multiple DataTemplate ListView - xaml

From my previous experience and some research on the web, I don't know if it's possible to have the following behavior:
<ContentPage.Content>
<AbsoluteLayout BackgroundColor="#2F767B">
<AbsoluteLayout x:Name="ScreenLayoutAdapter" BackgroundColor="#235A5E"
AbsoluteLayout.LayoutFlags="All">
<AbsoluteLayout.LayoutBounds>
<OnPlatform x:TypeArguments="Rectangle" Android="0, 0, 1, 1" iOS="1, 1, 1, 0.975" WinPhone="0, 1, 1, 1"/>
</AbsoluteLayout.LayoutBounds>
<ListView x:Name="ListViewCourses" BackgroundColor="Transparent" RowHeight="90"
AbsoluteLayout.LayoutBounds="0.5,1,0.9,0.9"
AbsoluteLayout.LayoutFlags="All">
<ListView.ItemTemplate>
<DataTemplate>
<ViewCell>
<AbsoluteLayout Margin="2">
<!-- Your design for the cell template -->
</AbsoluteLayout>
</ViewCell>
</DataTemplate>
</ListView.ItemTemplate>
</ListView>
</AbsoluteLayout>
</AbsoluteLayout>
</ContentPage.Content>
In the c# side, I then have a ObservableCollection<Item> that I bind with the ListViewCourses.
But now my question is:
When I touch an item, how can I change of DataTemplate cell?
I mean, it's a list of items but when I touch one, I want that the cell grows up and displays more information, about the item selected.
If I have to bring more information to make you understand, tell me
Thank in advance ! :)

What you could do, instead of changing the DataTemplate of a single item, is have all of the controls containing the extra information that you want to show, be set to IsVisible = false and then when it is clicked, set IsVisible = true on all the controls and also call ListView.ForceLayout() to force the ViewCell to get redrawn.
Finally, make sure ListView.HasUnevenRows = true or else the ViewCell will not change size.
Alternatively, you may be able to add the controls containing extra information to the ViewCell when it is selected, it may or may not be slower/faster depending on the number of items in the ListView, the amount of extra controls you are adding, and whether you need to query the DB or a service for that extra information.
Let me know if you have any questions/issues.
Edit: Actually, you will want to call ViewCell.ForceUpdateSize(), info here, in order to change the size of the ViewCell once selected. You also may need to store that ViewCell instance in a class level variable so that you can shrink it back down when the user clicks a different ViewCell in the list.
Edit #2: One last thing I have run into, is that on iOS, if you have enabled ListViewCachingStrategy.RecycleElement on your ListView, you will most likely not see any change in the ViewCell after making controls visible and calling ForceUpdateSize(). This might have something to do with the last paragraph here, but I am not sure how to properly fix/get around it at this time.
Edit #3: For example you might do something like this:
<ViewCell>
<!-- Use ListView.ItemTapped instead of the TapGestureRecognizer below -->
<!-- <ViewCell.GestureRecognizers>
<TapGestureRecognizer Tapped="OnViewCellTapped"/>
</ViewCell.GestureRecognizers> -->
<StackLayout>
<!-- Main info displayed by default -->
<StackLayout StyleId="DefaultStackLayout">
<Label Text="{Binding ItemName}"/>
<Label Text="{Binding ItemDates}"/>
<Label Text="{Binding ItemCredits}"/>
</StackLayout>
<!-- Extra info displayed upon selection -->
<StackLayout StyleId="ExtraStackLayout"
IsVisible="False">
<Label Text="{Binding ItemBuilding}"/>
<Label Text="{Binding ItemTeacher}"/>
<Label Text="{Binding ItemHours}"/>
</StackLayout>
</StackLayout>
</ViewCell>
Then when the user selects the cell, you would need to do something like this:
using System.Linq;
...
private void OnViewCellTapped(object sender, EventArgs args) {}
tapCount++;
ViewCell viewCellSender = (ViewCell)sender;
StackLayout rootStack = (StackLayout)viewCellSender.View;
StackLayout extraInfoStack = rootStack.Children.Where(stack => stack.StyleId == "ExtraStackLayout");
extraInfoStack.IsVisible = true;
viewCellSender.ForceUpdateSize();
}
I have not tested any of this or even attempted it before, so if the above does not work, I am confident that inserting the extraInfoStack element content once the ViewCell is clicked, will work. But give the above a try first. Obviously you will need to change the layouts if you want to use AbsoluteLayout and you will also need to change the OnViewCellTapped() casting code.
Edit #4: I usually try to steer clear of constant numbers for height and width values if possible but sometimes it is unavoidable. So for this all to work you are going to have to set ListView.HasUnevenRows = true and that means you will need to get rid of your ListView.RowHeight value completely since the rows need to be allowed to have variable height.
If at all possible, I would try to not set a constants height value, but if you absolutely cannot live without setting the height to a constants value, then you can probably give your ViewCell's inner AbsoluteLayout a HeightRequest of 120, but then you would need to change that value when the item is selected, after making your extra controls visible and before calling ViewCell.ForceUpdateSize(). Setting IsVisible to false is supposed to make it so that the control does not take up any space, so those extra controls should not mess with the sizing while they are hidden but I have never tried it myself.
If you run into issues, please let me know.

Related

Weird ImageButton's behavior on ListView

ImageButtons on a ListView is giving me a headache. Take a look at the gif here.
As you can see, the ImageButtons (3 vertical dots on the right of every ListView row) which were hidden gets super small as the ListView is scrolled up and down.
This is the corresponding XAML:
<ImageButton
Source="more_options"
HorizontalOptions="End"
WidthRequest="21"
BackgroundColor="Transparent"
Clicked="OnMoreOptionsTapped"
CommandParameter="{Binding .}"
Grid.Column="2"
Grid.Row="0"/>
Full XAML is here. ImageButton is at line 56.
I'm doing something wrong? How can I fix this?
Also, I know I could use just an Image with a TapGestureRecognizer but that's being a big issue for me, because I need access to the Image element to get it's coordinates and spawn the menu on the right location. Doing that with an ImageButton is a piece of cake, but it's not so easy with a TapGestureRecognizer. I did something like this:
XAML
<Image
Source="more_options"
Aspect="AspectFit"
HorizontalOptions="End"
WidthRequest="21"
BackgroundColor="Transparent"
Grid.Column="2"
Grid.Row="0">
<Image.GestureRecognizers>
<TapGestureRecognizer
Tapped="OnMoreOptionsTapped"
CommandParameter="{Binding .}"/>
</Image.GestureRecognizers>
</Image>
Code-behind:
private void OnMoreOptionsTapped(object sender, TappedEventArgs args)
{
var tapGesture = sender as TapGestureRecognizer;
var button = tapGesture.Parent as Image;
...
}
Basically, with an ImageButton element, the object sender is an ImageButton element, but with an Image, the object sender is a TapGestureRecognizer and I can't find a way to get the Image parent while I have only the TapGestureRecognizer child. Also, tapGesture.Parent is null and tapGesture.Parent.Parent is null too. I've tried both.
So:
ImageButtons on ListView are bugging out hard. Anyone have a fix?
Anyone could tell me how to get the Image parent having only the child TapGestureRecognizer?
I believe answering any of those two questions would solve my problem hehe.
Also, this is one of my first experiences asking questions here, I beg your pardon if I did something wrong.
Thanks all :)
The reason behind your issue is a bug that is currently in the xamarin listview which can be found here :
https://github.com/xamarin/Xamarin.Forms/issues/5200
Solution is to downgrade to v3.4 for now until we get an intimate from XF side
Goodluck revert if you have queries

Using x:Bind inside the GridView's ItemTemplate layout User Control in UWP

In the Universal Windows Platform API, how do I use x:Bind inside of a User Control (intended to be the layout for a GridView's ItemTemplate) to bind to instance properties of a GridView's ItemSource?
Background
I'm trying to re-create the layout found in Windows 10 stock apps like Sports, News, Money, etc.
I'm using a two GridViews for the main area of the app; one for "featured articles" (2 large photos w/ headlines) and one for all the other articles (smaller photos w/ headlines).
I'm able to bind to a data source that I supply in the code behind (a List where NewsItem is a POCO with a Image and Headline property) Here's the pertinent parts of the MainPage.xaml:
<Page ...
xmlns:data="using:NewsApp.Models" />
....
<GridView Name="FeaturedItems" Grid.Row="0">
<GridView.ItemTemplate>
<DataTemplate x:DataType="data:NewsItem">
<Grid Name="mainPanel" HorizontalAlignment="Stretch" Width="500" >
<Image Source="{x:Bind Image}" HorizontalAlignment="Stretch" />
<TextBlock Text="{x:Bind Headline}" />
</Grid>
</DataTemplate>
</GridView.ItemTemplate>
</GridView>
....
The Image and Headline bind just fine (even though they've not been styled correctly). However, instead I think I need to bind to a User Control to get the styling options I want, control over resizing esp. when using Visual State Triggers and to simplify the XAML in general (at least, this was the technique suggested to me.)
So, I added a new User Control to the project (FeaturedItemControl.xaml), and copied in the DataTemplate's child Grid:
<UserControl ... >
<Grid Name="mainPanel" HorizontalAlignment="Stretch" Width="500" >
<Image Source="{x:Bind Image}" HorizontalAlignment="Stretch" />
<TextBlock Text="{x:Bind Headline}" />
</Grid>
</UserControl>
And then back in the MainPage.xaml, I change the DataTemplate to reference the new FeaturedItemControl:
<GridView Name="FeaturedItems" Grid.Row="0">
<GridView.ItemTemplate>
<DataTemplate x:DataType="data:NewsItem">
<local:FeaturedItemControl HorizontalAlignment="Stretch" VerticalAlignment="Stretch" />
</DataTemplate>
</GridView.ItemTemplate>
</GridView>
However, I get the error message for both Image and Headline properties: Invalid binding path 'Headline': Property 'Headline' can't be found on type 'FeaturedItemControl'.
I've tried a few things but am flailing just throwing code at the problem without understanding what I'm doing. Any help would be greatly appreciated.
Thank you for your kind attention.
Using Depechie's answer, I formulated this little cheat cheat for posterity:
Do note that you MUST use this technique to utilize the VisualStateManager with items inside your data bound controls' (GridView, ListView) data templates.
1) Create a User Control.
2) Cut the content of the DataTemplate in your page and paste it into the User Control replacing the template's Grid.
3) Reference the User Control from inside the Data Template:
4) Modify the contents of the User Control changing x:Bind statements to utilize object.property notation:
<UserControl>
<StackPanel>
<Image Source="{x:Bind NewsItem.LeadPhoto}" />
<TextBlock Text="{x:Bind NewsItem.Headline}" />
<TextBlock Text="{x:Bind NewsItem.Subhead}" />
</StackPanel>
</UserControl>
5) Add this in the User Control's Code Behind:
public Models.NewsItem NewsItem { get { return this.DataContext as Models.NewsItem; } }
public ContactTemplate()
{
this.InitializeComponent();
this.DataContextChanged += (s, e) => Bindings.Update();
}
Well it's possible to use x:Bind in user controls, but you'll need to add some extra code behind.
I encountered the same problem in my project, you can see the result here : https://github.com/AppCreativity/Kliva/tree/master/src/Kliva/Controls
So what you need to do is, create a property in the code behind of your user control that points to the correct DataContext.
If you do that, you can use properties of that DataContext in the xaml of your control: for example:
Do note that in the constructor of your control you do need to add: DataContextChanged += (sender, args) => this.Bindings.Update(); because the datacontext will change depending on the page where your control is used!
Then on the page where you are placing this control, you'll also need to do the same to enable the x:bind to work.
You'll see this in my example on the MainPage.DeviceFamily-Mobile.xaml and MainPage.xaml.cs files.
Hope this helps.
x:Bind isn't really hierarchical like Binding/DataContext is. Additionally when you're not directly inside a DataTemplate (such as inside your user control) the object that x:Bind tries to use is 'this' rather than 'this.DataContext'. My current line of thinking on how to solve this sort of issue is to try not to use UserControls anywhere. Instead preferring DataTemplates contained within a ResourceDictionary. There are some pretty strong caveats to this approach though, you will for example crash the xaml compiler if you use x:Bind inside a data template that was created from the ResourceDictionary item template (add new item). you can find a pretty complete example here https://github.com/Microsoft/Windows-universal-samples/tree/master/Samples/XamlBind its important to note in the sample where they show the ResourceDictionary being used that its not actually just a ResourceDictionary.xaml its also a ResourceDictionary.xaml.cs (this is where the generated code from x:Bind ends up)
Another option is to add Headline and Image as properties on your user control and x:Bind them from the template, then inside the user control x:Bind as you are currently doing, but now the x:Bind generated path 'this.Headline' will exist. Unfortunately the order things are actually bound means that the x:Bind's you have inside your user control will have to be OneWay rather than the default OneTime. this is because x:Bind OneTime does the bind inside the InitializeComponent call, and any set of properties/DataContext stuff doesn't get done until after that has already run.
So to sum this up, you have two options, use data templates everywhere, or bind to properties that are directly on the user control.

Nested GridView Visibility is not working properly

I have a nested gridview. There seems to be a problem with the Visibility. In the code below, OuterCollection and InnerCollection have HideInUI property which I inverse to determine the visibility (ex. if true, hide).
However, on the 2nd HideInUI, if the 1st item is hidden, it seems to hide everything else.
<GridView x:Name="GridView_Outer"
InnerCollectionsSource="{Binding Path=OuterCollection}">
<GridView.InnerCollectionTemplate>
<DataTemplate>
<StackPanel Visibility="{Binding Path=HideInUI, Converter={StaticResource InverseBooleanToVisibilityConverter}}">
<Button Content="{Binding Path=Title}"
IsEnabled="False"
Style="{StaticResource CategoryButton}"/>
<GridView x:Name="GridView_Inner"
InnerCollectionsSource="{Binding Path=InnerCollection}">
<GridView.InnerCollectionTemplate>
<DataTemplate>
<Button Width="120"
Command="{Binding ElementName=GridView_Outer, Path=DataContext.SelectPaymentTypeCommand}"
CommandParameter="{Binding Path=PaymentAmountTypeID}"
Content="{Binding Path=ScreenTitle}"
Visibility="{Binding Path=HideInUI, Converter={StaticResource InverseBooleanToVisibilityConverter}}">
</Button>
</DataTemplate>
Edit
Sorry for the confusion. But what happens is.
Ex. Category 1 has Item 1, Item 2, Item 3. If HideInUI is true in Item1, Item2 and Item3 becomes hidden also. But if Item 2 or Item3 is HideInUI instead, it works properly. I can't seem to find out why.
The issue you have here is that you are setting the visibility of the StackPanel(in your outer gridview) that contains the rest of your solution. You have to look at it as boxes, everything within it is affected by its parents in some ways.
The tabulation is a great way to help you understand which block contains what, in your example, you can clearly see that your GridView contains one thing, which is a DataTemplate. This DataTemplate also contains one thing which is the StackPanel, and so on. In your case, your StackPanel contains everything else, so if you hide it, you hide everything in it also.
What I think you actually want, it is to hide the button in the "Outer GridView", in this case, you should set visibility of the "Title" button instead of setting the visibility of the whole StackPanel.
Cutting and pasting your code:
Visibility="{Binding Path=HideInUI, Converter={StaticResource InverseBooleanToVisibilityConverter}}"
from your StackPanel to your "Title" button should do the trick.

Keep element in view while scrolling

Simpel question, I have a windows phone page that contains a scrollviewer with inside it an image, a textblock and a richtextbox.
Now when the user starts scrolling I want to keep the textblock in view on top when the image has scrolled outside the page.
So the effect is, user starts scrolling upwards, everything scrolls upwards, when the image is outside the page, the textblock stays at the top of the page but the richtextbox keeps scrolling upwards.
Any thoughts?
Here is a way to reach this result:
First, the layout. I've set a grid, with two rows. The first is empty, and will host the header when we need to freeze it. The second row contains the scrollviewer.
Inside the scrollviewer, I've put the controls in a grid, but you can use whatever container suits you.
<ScrollViewer Grid.Row="1"
Margin="0"
Padding="0"
x:Name="ParentScroll"
ManipulationMode="Control"
MouseMove="ParentScroll_MouseMove">
<Grid x:Name="ChildGrid">
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
<RowDefinition />
</Grid.RowDefinitions>
<Image Source="Picture.jpg" Grid.Row="0"/>
<TextBlock Text="Header" Grid.Row="1" x:Name="TextHeader" />
<RichTextBox Grid.Row="2" x:Name="RichText">
<Paragraph>
<Bold>RichTextBox</Bold>
<!-- More stuff -->
</Paragraph>
</RichTextBox>
</Grid>
</ScrollViewer>
I use the MouseMove event to be notified of the scrolling event. You can also dig into the template, extract the ScrollBar control, and subscribe to the ValueChanged event, as described here: http://social.msdn.microsoft.com/Forums/wpapps/en-US/81fcd34e-6ec9-48d0-891e-c53a53344553/scrollviewer-synchronization
Note that you need to set ManipulationMode to Control or the position of the controls won't be updated at a smooth rate. I guess it's due to some internal optimization.
In the code behind, I use the TransformToVisual method to compute the relative position of the controls to the ScrollViewer. This way, I can know when the header goes out of view. When it does, I remove it from the child grid, and put it outside of the ScrollViewer, in the parent grid. When the top of the RichTextBox goes out of view, I put the header back into the ScrollViewer:
private void ParentScroll_MouseMove(object sender, System.Windows.Input.MouseEventArgs e)
{
if (Grid.GetRow(this.TextHeader) == 1)
{
var generalTransform = TextHeader.TransformToVisual(ParentScroll);
var childToParentCoordinates = generalTransform.Transform(new Point(0, 0));
if (childToParentCoordinates.Y < 0)
{
this.ChildGrid.Children.Remove(this.TextHeader);
this.ParentGrid.Children.Add(this.TextHeader);
Grid.SetRow(this.TextHeader, 0);
}
}
else
{
var generalTransform = RichText.TransformToVisual(ParentScroll);
var childToParentCoordinates = generalTransform.Transform(new Point(0, 0));
if (childToParentCoordinates.Y > 0)
{
this.ParentGrid.Children.Remove(this.TextHeader);
this.ChildGrid.Children.Add(this.TextHeader);
Grid.SetRow(this.TextHeader, 1);
}
}
There may be less-hacky ways to reach the same results, but this solution seems to work smoothly in the emulator.
I've found a working solution myself... the complete detail is available on my blog here... it contains also the link to my demo project on GitHub.
The trick was to get hold of the VerticallScrollBar inside the ScrollViewer and to set the ManipulationMode to Control to get enough feedback on the UI thread.
With the scroll offset information of the scrollbar we than animate the specific ui element we want to keep in view.

How to get the index of an item in an itemscontrol

I want to show the current index of an item in an itemscontrol:
<TextBlock Foreground="#ffffffff" Margin="8,8,2,2" TextWrapping="Wrap" Text="{Binding RelativeSource={RelativeSource Mode=TemplatedParent}, Path=Items.CurrentIndex}" Grid.Column="0" Grid.Row="0" HorizontalAlignment="Right"/>
This is my best guess. I've come across many possible solutions, but working with alternationcount (not supported in silverlight as it seems) or other didn't give me a result.
The itemscontrol looks like this:
<ItemsControl Grid.Column="0" Grid.ColumnSpan="3" HorizontalAlignment="Stretch" Grid.Row="6" ItemsSource="{Binding Alternatives, Mode=TwoWay}" ></ItemsControl>
The list bound to the itemscontrol is a simple object with some properties.
I really like to do this in XAML, as we reuse that object on alot of pages.
Any good suggestions would be great.
PS: I don't want the index after interaction from the user, it should be retrieved automatically.
if you want the index of an item in an itemscontrol - it just make sense for me if there is some sort of selection. but the itemsscontrol has no selector and so no selection and so no selectionchanged event. so if you want some selection take listbox
one more thing
<ItemsControl ItemsSource="{Binding Alternatives, Mode=TwoWay}" ></ItemsControl>
mode=twoway makes no sense, cause your itemsscontrol will never set the underlining source.
EDIT: i just assume what you want from your comment. you can use a ICollectionView to iterate through your items. but your Itemscontrol can not show this of course. but you can alter the item itself to show the iteration
ICollectionView view = CollectionViewsource.GetDefaultView(Alternatives);
view.CurrentChanged += (s,a) =>
{
var current = this.view.CurrentItem;
//to stuff with your current item here
};
elsewhere where you want to iterate
view.MoveCurrentToNext();
you should handle first and last item and that stuff too.