Add click gesture recognizer on stack child (item in scroll) - c#

I've got a stack layout inside a gridview, that I use as a sort of list.
The "items" in the list have to be click/tap -able, but I can't find a way to make the child area clickable while also giving a value to the event to know what item the user clicked.
Right now I create 20 items in a for loop. The "i" is an integer used for counting.
I put an BoxView over the original boxview and label of the item, which is transparent and gets the gesturerecognizer.
var clickableBoxv = new BoxView
{
BackgroundColor = Color.Transparent,
Margin = new Thickness(0, 5, 0, 5)
};
clickableBoxv.GestureRecognizers.Add(new TapGestureRecognizer
{
Command = new Command(() => Item_Clicked(i)),
});
private void Item_Clicked(int num)
{
DisplayAlert("Alert", num.ToString(), "OK");
}
But when I click the item, it shows an alert with only the last added number. (which didn't surprise me). But how can I get the alert to show me the specific item number?

While I haven't tested your exact code I have reproduced the same behaviour with some Actions inside a for loop.
Option 1 - Track the boxes.
It might not be the most ideal of solutions but one option would be to keep track of your boxes and use their index in the collection to represent the number.
// 1. A place to store the boxes.
IList<BoxView> boxes = new List<BoxView>();
var clickableBoxv = new BoxView
{
BackgroundColor = Color.Transparent,
Margin = new Thickness(0, 5, 0, 5)
};
// 2. Keep track of your clickable boxes.
boxes.Add(clickableBoxv);
clickableBoxv.GestureRecognizers.Add(new TapGestureRecognizer
{
// 3. Pass in the box rather than the int.
Command = new Command(() => Item_Clicked(clickableBoxv)),
});
private void Item_Clicked(BoxView box)
{
// 4. Use the index as the number.
DisplayAlert("Alert", boxes.IndexOf(box).ToString(), "OK");
}
Option 2 - sub class BoxView
// 1. Sub class
public class MyBoxView : BoxView
{
public int Index { get; set; }
}
// 2. Use new sub class
var clickableBoxv = new MyBoxView
{
BackgroundColor = Color.Transparent,
Margin = new Thickness(0, 5, 0, 5),
Index = i,
};
clickableBoxv.GestureRecognizers.Add(new TapGestureRecognizer
{
// 3. Pass in the box rather than the int.
Command = new Command(() => Item_Clicked(clickableBoxv)),
});
private void Item_Clicked(MyBoxView box)
{
// 4. Use the index as the number.
DisplayAlert("Alert", box.Index.ToString(), "OK");
}

Related

Xamarin.Forms CollectionView View Recycling Issues

So I am using a CollectionView to display a list of entities that, when pressed, toggle between a opened and closed state. I did this by animating the HeightRequest parameter of the parent container of the view that was pressed, then adding whatever views I wanted to show in the view's expanded state. Here is the code snippet for that:
var animate = new Animation(d => this.HeightRequest = d,
this.Bounds.Height, this.Bounds.Height + 300, Easing.CubicOut);
animate.Commit(this, "a", length: 500);
this.layout.Children.Add(this.candidateList);
this.layout.Children.Add(this.openButton);
This works fine, however, if I scroll down the list, I see that there are views that are also expanded even though I did not touch them previously, more so every full page of scrolling later. Some even include the views that I've added to the expanded state, showing incorrect data. I have assumed that this is due to the recycling mechanics of the CollectionView work in order to save on rendering costs, but there must be some way to fix this. Here is the code for relevant views:
var officesList = new CollectionView
{
ItemTemplate = new DataTemplate(typeof(OfficeListView)),
HorizontalOptions = LayoutOptions.FillAndExpand,
ItemsLayout = new LinearItemsLayout(ItemsLayoutOrientation.Vertical)
{
SnapPointsType = SnapPointsType.None,
ItemSpacing = 10,
},
Margin = new Thickness(20, 5),
ItemSizingStrategy = ItemSizingStrategy.MeasureAllItems,
Footer = " ",
FooterTemplate = new DataTemplate(() =>
{
return new StackLayout
{
Margin = new Thickness(20, 10),
Children = {
new LocorumLabels.MediumLabel
{
Text = "x Results | No filters applied" //TODO: Bind these to footer
}
}
};
})
};
The next snippet is the CollectionsView within an expanded "Office":
this.candidateList = new CollectionView
{
ItemTemplate = new DataTemplate(typeof(CandidateDetailView)),
HorizontalOptions = LayoutOptions.FillAndExpand,
ItemsLayout = new LinearItemsLayout(ItemsLayoutOrientation.Vertical)
{
SnapPointsType = SnapPointsType.None,
//ItemSpacing = 10,
},
Margin = new Thickness(10, 5),
ItemSizingStrategy = ItemSizingStrategy.MeasureAllItems,
Footer = " ",
HeightRequest = 300
};
And here is a video showing what is going on:
https://youtu.be/Ltg2o8BwfwY
Hopefully someone can let me know of a solution. Many thanks.
You are right on the cause.
As CollectionView uses DataTemplate you need to set your views and your data in such a way that when recycled the view appears as it should.
The non-obvious part to keep the animation working is to call the animation as of now but when it completes to set the value in data that would alter the view from the state before the animation to the state after the animation.

Can't find variable for button inside if/else

I want to have a different margin for iOS and Android so I tried to make if/else but the MyButton cannot be found when then button is inside the if/else like this:
if (Device.RuntimePlatform == Device.iOS)
{
var MyButton = new Button
{
Margin = new Thickness(0, -15, 0, 0)
};
}
else
{
var MyButton = new Button
{
Margin = new Thickness(-10, -15, 0, 0)
};
}
var MyStackLayout = new StackLayout
{
Children = { MyButton }
};
The name MyButton does not exist in the current context.
Is there a work around or a different method for this?
change your code as below:
Button MyNewButton;
if (Device.RuntimePlatform == Device.iOS)
{
MyNewButton= new Button
{
Margin = new Thickness(0, -15, 0, 0)
};
}
else
{
MyNewButton= new Button
{
Margin = new Thickness(-10, -15, 0, 0)
};
}
var MyStackLayout = new StackLayout
{
Children = { MyNewButton}
};
When you define a variable in if body, it cannot be access in out of if body.
And if you declare variable with the same name (Mybutton), change this button name.
As it was mentioned multiple times, the problem is that MyButton is living in a wrong scope. What is a scope?
The scope of a variable determines its visibility to the rest of a
program. In the examples throughout the C# Fundamentals tutorial,
variables have been defined within methods. When created in this way,
the scope of the variable is the entire method after the declaration.
This means that the variable is available to use within the method but
when control passes to another method the variable is unavailable.
You can read more about it here.

Show listview in middle of method but wait until SelectionChanged event fires before method continues

Windows UWP app in C#. I have a method that checks a condition and depending on the condition, it may need to show a listview to the user so they can select an item from the list. I have more code in the method, after I potentially show the list view that needs to run after. However, because the listview shows and I have to wait for the SelectionChanged event handler to fire, I cannot figure out how to pause the calling method on that line until the event handler is completed for SelectionChanged. I don't have code written yet, so here is some pseduo code to illustrate:
private void LookupEmployee(string searchCriteria)
{
List<string> matches = GetEmployeeNameMatchesFromCriteria(searchCriteria);
if(matches.Count == null || matches.Count == 0)
{
//No matches found, warn user
return;
}
if(matches.Count == 1)
{
//We are good, we have just one match which is desirable.
}
if(matches.Count > 1)
{
//This means we have more than one match and we need to popup list view to have user select one
ShowListView(matches);
}
//Process Employee data here.
}
I know one option is to "daisy chain" calls by breaking out the final processing of employee data to another method and call that from the event handler for SelectionChanged of the listview. However, this has two issues. First, if I just have one match, then I will not be showing the listview or getting the SelectionChanged anyway. Second, if I had a bunch of variables and other things at the beginning of the method to be used at the end of the method, I don't want to (nor do I know how to) pass all of that through and back from the event handler in the event I need to show it.
I guess what I am looking for in a way is how the MessageDialog is handled.
var md = new MessageDialog("My Message");
md.Commands.Add(new UICommand("Okay")
{
});
var result = await md.ShowAsync();
if (result.Label == "Okay")
{
DoStuff;
}
Where using this will wait on the line:
await md.ShowAsync();
Until the user clicks the button, at which point the method can continue from there.
I guess I am looking for something similar to that where I can hold on the line of the method in the case that I need to show the listview until the user selects and item and grab the item that was selected.
Thoughts?
Thanks!
Okay, I think I found what I was looking for so I wanted to post the code. This is similar to how a modal window worked in the old days. Basically, you can use a ContentDialog which will allow you to "wrap" any controls you want in it. In my case, I want to show a ListView, so I wrap that in the ContentDialog. Here is what I have:
First we can do our tests and based on the tests, we can create the ContentDialog/ListView if needed. If we do create the ContentDialog, we can also setup the Display parameters so it fits the way we want it to.
private async void checkProductMatches()
{
var selectedItem = string.Empty;
//Check our results from DB.
if (productResults.Count == 0)
{
//This means we didn't find any matches, show message dialog
}
if (productResults.Count == 1)
{
//We found one match, this is ideal. Continue processing.
selectedItem = productResults.FirstOrDefault().Name;
}
if (productResults.Count > 1)
{
//Multiple matches, need to show ListView so they can select one.
var myList = new ListView
{
ItemTemplate = Create(),
ItemsSource =
productResults,
HorizontalAlignment = HorizontalAlignment.Stretch,
VerticalAlignment = VerticalAlignment.Stretch
};
var bounds = Window.Current.Bounds;
var height = bounds.Height;
var scroll = new ScrollViewer() { HorizontalAlignment = HorizontalAlignment.Stretch, VerticalAlignment = VerticalAlignment.Stretch, Height = height - 100 };
var grid = new StackPanel();
grid.Children.Add(myList);
scroll.Content = grid;
var dialog = new ContentDialog { Title = "Title", Content = scroll };
Now, we wire up the event handler for the ListView SelectionChanged event and grab the selectedItem should this event raise.
myList.SelectionChanged += delegate (object o, SelectionChangedEventArgs args)
{
if (args.AddedItems.Count > 0)
{
MyProducts selection = args.AddedItems[0] as MyProducts;
if (selection != null)
{
selectedItem = selection.Name;
}
}
dialog.Hide();
};
Finally, we await the showing of the ContentDialog.
var s = await dialog.ShowAsync();
What this will do is, if we have one item, there is no need to popup the content dialog. So, we can assign the one result to our selectedItem variable and proceed. However, if we have multiple matches, we want to display a list for the user to select the one item. In this case, we create the ContentDialog, ListView and display parameters. They key is to wire up the event handler before we call to show the dialog and inside of the event handler, we make sure to cancel or close the dialog. Then we call to await the dialog showing. This will pause execution of this method on that line while the dialog is showing. Once the user selects an item, the event handler will raise, get the selected item and then close the dialog, which will then allow the method to continue execution from the awaited line.
Here is the full method:
private async void checkProductMatches()
{
var selectedItem = string.Empty;
//Check our results from DB.
if (productResults.Count == 0)
{
//This means we didn't find any matches, show message dialog
}
if (productResults.Count == 1)
{
//We found one match, this is ideal. Continue processing.
selectedItem = productResults.FirstOrDefault().Name;
}
if (productResults.Count > 1)
{
//Multiple matches, need to show ListView so they can select one.
var myList = new ListView
{
ItemTemplate = Create(),
ItemsSource =
productResults,
HorizontalAlignment = HorizontalAlignment.Stretch,
VerticalAlignment = VerticalAlignment.Stretch
};
var bounds = Window.Current.Bounds;
var height = bounds.Height;
var scroll = new ScrollViewer() { HorizontalAlignment = HorizontalAlignment.Stretch, VerticalAlignment = VerticalAlignment.Stretch, Height = height - 100 };
var grid = new StackPanel();
grid.Children.Add(myList);
scroll.Content = grid;
var dialog = new ContentDialog { Title = "Title", Content = scroll };
myList.SelectionChanged += delegate (object o, SelectionChangedEventArgs args)
{
if (args.AddedItems.Count > 0)
{
MyProducts selection = args.AddedItems[0] as MyProducts;
if (selection != null)
{
selectedItem = selection.Name;
}
}
dialog.Hide();
};
var s = await dialog.ShowAsync();
}
//Test furter execution. Ideally, selected item will either be the one record or we will
//get here after the list view allows user to select one.
var stringTest = string.Format("Selected Item: {0}", selectedItem);
}
Hope this helps someone.

Create line use BoxView in Xamarin Forms

I need to create a line which is a made up of multiple BoxViews (WidthRequest=0.20830 and HeightRequest=5 for every BoxView). There will be 1440 BoxViews in a sequence arranged in way that it will create a line(approximately WidthRequest=300).
My Code -
public partial class timeManagement : ContentPage
{
double oneMinute=0.20833333;
public timeManagement ()
{
InitializeComponent ();
StackLayout stack = new StackLayout{Orientation=StackOrientation.Horizontal,
};
for(int i=1;i<=14;i++)
{
BoxView piece_ofLine = new BoxView
{
HeightRequest=5,
WidthRequest=5,
Color=Color.Red
};
if (i >= 5 && i <= 9) {
stack.Children.Add (piece_ofLine);
piece_ofLine.Color = Color.Green;
} else {
stack.Children.Add (piece_ofLine);
piece_ofLine.Color = Color.Red;
}
}
Content = new StackLayout {
Padding =50,
Spacing=0,
Children = {
stack
}
};
}
}
And output is-
But I want all boxes side by side so it will appear like a single line.
There are a few issues here
The default orientation for a StackLayout is Vertical so you would need to set that on the stack variable
You would need to create a new instance for each BoxView that you add to the stack. Otherwise it will just keep adding the same one over and over. And in the end you would just have one.
I assume that you would want them directly to the side of each other. If this is the case, I think that it would be safest to explicitly set the Spacing of your StackLayout to 0

Changing Xamarin Label in ListCell does not work

I have a problem with a ListView. I want each Cell to have a label and a switch but the text of the label does not appear.
Here is my code:
public class FilterPage : ContentPage
{
public FilterPage()
{
List<FilterCell> listContent = new List<FilterCell>();
foreach(string type in Database.RestaurantTypes)
{
FilterCell fc = new FilterCell();
fc.Text = type;
listContent.Add(fc);
}
ListView types = new ListView();
types.ItemTemplate = new DataTemplate(typeof(FilterCell));
types.ItemsSource = listContent;
var layout = new StackLayout();
layout.Children.Add(types);
Content = layout;
}
}
public class FilterCell : ViewCell
{
private Label label;
public Switch CellSwitch { get; private set; }
public String Text{ get { return label.Text; } set { label.Text = value; } }
public FilterCell()
{
label = new Label();
CellSwitch = new Switch();
var layout = new StackLayout
{
Padding = new Thickness(20, 0, 0, 0),
Orientation = StackOrientation.Horizontal,
HorizontalOptions = LayoutOptions.FillAndExpand,
Children = { label, CellSwitch }
};
View = layout;
}
}
If I enter a fixed Text in the FilterCell-Constructor it works fine (e.g.: label.Text = "Hello World")
When I create a Method for the ItemSelected-Event and read out the SelectedItem.Text Property I get the text I assigned as Value but it's never displayed. Only the switch is displayed when I try to run this Code.
Thanks for your help
Niko
Ohh boy. This code looks like a rape (sorry I had to say this).
Now let's see what's wrong:
The reason is you are mixing up data and view heavily.
The line
types.ItemTemplate = new DataTemplate(typeof(FilterCell));
means: "For each item in the list (ItemsSource) create a new filter cell". The FilterCells that you create in the loop are never displayed.
The easy fix
public class FilterPage : ContentPage
{
public FilterPage()
{
var restaurantTypes = new[] {"Pizza", "China", "German"}; // Database.RestaurantTypes
ListView types = new ListView();
types.ItemTemplate = new DataTemplate(() =>
{
var cell = new SwitchCell();
cell.SetBinding(SwitchCell.TextProperty, ".");
return cell;
});
types.ItemsSource = restaurantTypes;
Content = types;
}
}
There is a standard cell type that contains a label and a switch SwitchCell, use it.
As ItemsSource of your list, you have to use your data. In your case the list of restaurant types. I just mocked them with a static list.
The DataTemplate creates the SwitchCell and sets the Databinding for the Text property. This is the magic glue between View and data. The "." binds it to the data item itself. We use it, because our list contains items of strings and the Text should be exactly the string. (read about Databinding: https://developer.xamarin.com/guides/xamarin-forms/getting-started/introduction-to-xamarin-forms/#Data_Binding )
I striped away the StackLayout that contained the list. You can directly set the list as Content of the page.
Lesson
use standard controls, if possible
You should always try to remember to keep data and view apart from each other and use data binding to connect to each other.
Try to avoid unnecessary views.

Categories