jeudi 10 septembre 2020

UWP using ChangeView() on a ScrollViewer so that the focused item can be placed with a specific offset [C#]

The initial need was that I needed to be able to sync two listview together, however one of these listview had horizontal items and was not selectable which meant that it would not inform the first listview that the selected view had changed. 

The first listview is not only and indicator but can also allow to quickly access a certain row in our main listview, here is quick screenshot to better understand what I wanted to do, in red the two selected items that need to be sync and in the other colors that other ListView.

The main issue was to be able to detect when the user scrolled down on the second ListView Vertical and to also know at what index position he was at in the listview.

First we need to get the ScrollView of our ListView to detect when the user was scrolling.
We will create class ListViewBaseExtension.cs which will hold the following method
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
 public static ScrollViewer GetScrollViewer(this DependencyObject element)
{
	if (element is ScrollViewer)
	{
		return (ScrollViewer)element;
	}

	for (int i = 0; i < VisualTreeHelper.GetChildrenCount(element); i++)
	{
		var child = VisualTreeHelper.GetChild(element, i);

		var result = GetScrollViewer(child);
		if (result == null)
		{
			continue;
		}
		else
		{
			return result;
		}
	}

	return null;
}
This method uses VisualTreeHelper which can allow us to easily access the UI visual tree.
Next, we will use the GetScrollViewer() method on our ListView to get our ScrollViewer:
//our second vertical listview
AppListView.Loaded += (sender, e) =>
{
	//getting scrollview
	ScrollViewer scrollViewer = AppListView.GetScrollViewer(); //Extension method
	if (scrollViewer != null)
	{
		scrollViewer.ViewChanging += ScrollViewerListView_ViewChanging;
	}
};
Now let's look at our method called ScrollViewerListView_ViewChanging that handles the ViewChanging event from our scrollViewer.
Everytime we will need to get the position of our items in the list using GetAllItemsPositions(), also we need to calculate the height that that user has scrolled down which will give us a currentVerticalPosition.  We will then use this to find an item that is on this position, which will then allow us to inform which item should be selected on the first listview:
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
private void ScrollViewerListView_ViewChanging(object sender, ScrollViewerViewChangingEventArgs e)
{
        //Offset of first listview
	double AdditionOffSetToAdd = -200;

	MyItemPositions = AppListView.GetAllItemsPositions();


	if (MyItemPositions != null)
	{
		var currentVerticalPosition = e.FinalView.VerticalOffset + AdditionOffSetToAdd;

		double itemIndex = MyItemPositions.Values.Where(a => a >= currentVerticalPosition).FirstOrDefault();

		CurrentVisibleItemIndex = MyItemPositions.FirstOrDefault(x => x.Value == itemIndex).Key;

		//Debug.WriteLine($"CurrentVisibleItemIndex :{CurrentVisibleItemIndex}");
		//Debug.WriteLine($"previousItemIndex :{previousItemIndex}");

		if (previousItemIndex != CurrentVisibleItemIndex)
		{
			//update previous
			previousItemIndex = CurrentVisibleItemIndex;

			//Debug.WriteLine("VerticalOffset :{0}", e.FinalView.VerticalOffset);
			//Debug.WriteLine("possible visible item {0}", CurrentVisibleItemIndex);

			CurrentItemIndexChangedCommand();
		}
	}
}

public event EventHandler CurrentItemIndexChanged;
private void CurrentItemIndexChangedCommand()
{
	CurrentItemIndexChanged?.Invoke(this, new EventArgs());
}
By listening to the event CurrentItemIndexChanged, we can now update our selected item on on first listview.

You can find the sample application here: https://github.com/Delaire/Samples/tree/master/SyncTwoListviews