.NET UI Virtualization Best Practices — How to Get Offset
Tags: webdev, devops, dotnet, tutorial
Published: 2023-07-26 20:11:17

---
canonical_url: https://developer.mescius.com/blogs/net-ui-virtualization-best-practices-how-to-get-offset
description: Learn about .NET UI virtualization best practices from ComponentOne. This tutorial will show how to get offset of an item in a large list of items.
---
When developing items-controls like ListView, TreeView, or Datagrid, it is important for the layout to be extremely fast at loading and updating because the control can potentially have a large number of items. To achieve optimal performance, we use a technique called “UI Virtualization”, which basically recycles the UI elements as the user scrolls through items.

But UI Virtualization can introduce new problems that you wouldn’t have otherwise - such as obtaining the offset position of an item deep within the list. In this blog, we will explain how to overcome it in your own custom control. For more background on why we use .NET UI virtualization, check out my previous blog [Analyzing .NET WPF Datagrid Performance Benchmarks](/blogs/analyzing-net-wpf-datagrid-performance-benchmarks "/blogs/analyzing-net-wpf-datagrid-performance-benchmarks").

## The Offset Problem with UI Virtualization<span class="css-1afrefi" aria-label="Copy"></span>

To better understand this problem, let's imagine we have a big list of numbers, and we want to know the offset (or position) of a specific index.

<div class="rich-media-item mediaSingleView-content-wrap image-center css-1hiznpy" data-layout="center" data-node-type="mediaSingle">

<div class="css-1eygpie">

<div data-type="file" data-node-type="media" data-width="813" data-height="186" data-id="57841361-98ae-4c93-80ab-55814eefc632" data-collection="contentId-3334078468" data-file-name="image-20220712-161202.png" data-file-size="4777" data-file-mime-type="image/png" data-alt="" data-context-id="3334078468">

<div id="newFileExperienceWrapper" class="new-file-experience-wrapper css-1kh3vb3" data-testid="media-card-view">

<div class="media-file-card-view css-1yn77bd" data-testid="media-file-card-view" data-test-status="complete" data-test-media-name="image-20220712-161202.png" data-test-progress="1">![.NET UI Virtualization](https://files.grapecity.com/gc-website-media/422l2ues/01-offset.png?width=500&height=114.39114391143912)</div>

</div>

</div>

</div>

</div>

Since we are using UI virtualization, it means that not all elements in the list are rendered at once, and therefore, we can’t simply obtain the offset. It requires computing to get the position of the elements. When the size of the elements is fixed, the computing of the position is trivial; multiplying the index of the item by the fixed length resolves it, but if every element has a different size, or the size depends on the laid out child, computing the position of an element depends on the size of all the previous elements, and summing up all the previous lengths becomes a performance problem when rendering a large number of items.

In addition to the performance issue, when the number of items is big, let’s suppose 1 billion items, storing the length of every item generates a memory issue too. In most cases, when binding to such a large number of items, it’s unnecessary to store these values since most items are never visited. Instead, an estimated size can be used until the item is finally visited.

## The Offset Solution<span class="css-1afrefi" aria-label="Copy"></span>

Since summing every item previous to the one we want is expensive, we need a way to have certain portions already summed. To achieve this, we can have a tree structure with fixed array-lengths where a parent node caches the aggregated value of its children.

<div class="rich-media-item mediaSingleView-content-wrap image-center css-1hiznpy" data-layout="center" data-node-type="mediaSingle">

<div class="css-mt9ph0">

<div data-type="file" data-node-type="media" data-width="690" data-height="376" data-id="51cddbf4-4cc6-4a91-806b-b2d32666b073" data-collection="contentId-3334078468" data-file-name="image-20220712-163722.png" data-file-size="6675" data-file-mime-type="image/png" data-alt="" data-context-id="3334078468">

<div id="newFileExperienceWrapper" class="new-file-experience-wrapper css-1hy9wd0" data-testid="media-card-view">

<div class="media-file-card-view css-1yn77bd" data-testid="media-file-card-view" data-test-status="complete" data-test-media-name="image-20220712-163722.png" data-test-progress="1">![.NET UI Virtualization](https://files.grapecity.com/gc-website-media/cdfh3rob/02-offset.png?width=500&height=272.463768115942)</div>

</div>

</div>

</div>

</div>

The positions of the item in the structure are calculated mathematically from the index in the collection by the following code:

```
    private int[] GetIndexArray(int index)
    {
        var indexArray = new int[_depth];
        for (int i = _depth - 1; i >= 0; i--)
        {
            indexArray[i] = index % _partitionLength;
            index = index / _partitionLength;
        }
        return indexArray;
    }
```

The returned array contains the indexes to navigate in each level to find the item.

For example, if the array-size (of the partition) is 3 and the searched index is 8, the returned array is [2,2], meaning it will sum all up to the index 2, and then again 2 more in the next level down.

<div class="rich-media-item mediaSingleView-content-wrap image-center css-1uqdzte" data-layout="center" data-node-type="mediaSingle">

<div class="css-6ofi5x">

<div data-type="file" data-node-type="media" data-width="570" data-height="276" data-id="233f6ec2-32e5-4909-a75e-51ffa26b78bf" data-collection="contentId-3334078468" data-file-name="image-20230707-215845.png" data-file-size="12822" data-file-mime-type="image/png" data-alt="" data-context-id="3334078468">

<div id="newFileExperienceWrapper" class="new-file-experience-wrapper css-s3yhm3" data-testid="media-card-view">

<div class="media-file-card-view css-1yn77bd" data-testid="media-file-card-view" data-test-status="complete" data-test-media-name="image-20230707-215845.png" data-test-progress="1">![.NET UI Virtualization](https://files.grapecity.com/gc-website-media/xjebv3gp/03-offset.png?width=500&height=242.1052631578947)</div>

</div>

</div>

</div>

</div>

It will sum up 15+14+1+6=36\. Notice we’re summing 4 numbers instead of 8.

This sample is using an array of length 3 for graphical reasons but in real cases, the arrays can be much bigger, so the performance gains are more noticeable.

On the other hand, this data structure can be used to reduce the memory by defining a default-length value and avoiding storing all the values that are equal to the default. For instance, let’s suppose the default value is 5, and we have a list of 9 values. Initially, the top level node will store the value 45, 9 items x 5 in length. And when setting values that are different from 5, only those values are saved in the structure. Let’s suppose the second item is set to 8.

<div class="rich-media-item mediaSingleView-content-wrap image-center css-13opbv3" data-layout="center" data-node-type="mediaSingle">

<div class="css-1mwlaiw">

<div data-type="file" data-node-type="media" data-width="628" data-height="351" data-id="e908d87b-06cb-40ba-850a-9229ac60b05d" data-collection="contentId-3334078468" data-file-name="image-20220712-185352.png" data-file-size="5000" data-file-mime-type="image/png" data-alt="" data-context-id="3334078468">

<div id="newFileExperienceWrapper" class="new-file-experience-wrapper css-ijmqr8" data-testid="media-card-view">

<div class="media-file-card-view css-1yn77bd" data-testid="media-file-card-view" data-test-status="complete" data-test-media-name="image-20220712-185352.png" data-test-progress="1">![.NET UI Virtualization](https://files.grapecity.com/gc-website-media/kaugdcc2/04-offset.png?width=500&height=279.45859872611464)</div>

</div>

</div>

</div>

</div>

This way, we only allocate memory for the items that are different from the default.

## Performance Comparison<span class="css-1afrefi" aria-label="Copy"></span>

Within our ComponentOne libraries, we’ve included a List class that implements this feature called C1LengthList.

To visualize the benefits of using this structure, we’ve made a benchmarking app that executes, on the one hand, the sum of the elements of a List to get the offset and, on the other hand, the GetOffset method of C1LengthList.

<div class="pm-table-container with-shadow-observer" data-layout="default">

<div class="pm-table-wrapper">

<table border="0" data-number-column="false">

<tbody>

<tr>

<th class="ak-renderer-tableHeader-sortable-column__wrapper" colspan="1" rowspan="1" data-colwidth="122" aria-sort="none">

<div class="ak-renderer-tableHeader-sortable-column">

Size

</div>

</th>

<th class="ak-renderer-tableHeader-sortable-column__wrapper" colspan="1" rowspan="1" data-colwidth="168" aria-sort="none">

<div class="ak-renderer-tableHeader-sortable-column">

 List (time in us)

</div>

</th>

<th class="ak-renderer-tableHeader-sortable-column__wrapper" colspan="1" rowspan="1" data-colwidth="168" aria-sort="none">

<div class="ak-renderer-tableHeader-sortable-column">

C1LengthList (time in us)

</div>

</th>

</tr>

<tr>

<td colspan="1" rowspan="1" data-colwidth="122">

1000

</td>

<td colspan="1" rowspan="1" data-colwidth="168">

50.1

</td>

<td colspan="1" rowspan="1" data-colwidth="168">

169.1

</td>

</tr>

<tr>

<td colspan="1" rowspan="1" data-colwidth="122">

10000

</td>

<td colspan="1" rowspan="1" data-colwidth="168">

406.8

</td>

<td colspan="1" rowspan="1" data-colwidth="168">

157.8

</td>

</tr>

<tr>

<td colspan="1" rowspan="1" data-colwidth="122">

100000

</td>

<td colspan="1" rowspan="1" data-colwidth="168">

4103.8

</td>

<td colspan="1" rowspan="1" data-colwidth="168">

157.1

</td>

</tr>

<tr>

<td colspan="1" rowspan="1" data-colwidth="122">

1000000

</td>

<td colspan="1" rowspan="1" data-colwidth="168">

48021.0

</td>

<td colspan="1" rowspan="1" data-colwidth="168">

181.3

</td>

</tr>

</tbody>

</table>

</div>

</div>

<div data-source="[&quot;84aae9c2-50a2-494d-80e7-b2402a3509f3&quot;]" data-mark-type="dataConsumer" data-renderer-mark="true">

<div class="ak-renderer-extension  " data-layout="default">

<div class="ak-renderer-extension-overflow-container">

<div class="css-lle5zh" data-macro-id="1a7fe4c7-cbbd-440c-95a9-2d6fc1c3deda">

<div class="css-ic10z6 e16fktb14">

<div>

<div>

<div>

<div class="css-11i3kwc e1am5oyd0">![.NET UI Virtualization](https://files.grapecity.com/gc-website-media/vsdizs3z/05-offset.png?width=500&height=289.98699609882965)</div>

</div>

</div>

</div>

</div>

</div>

</div>

</div>

</div>

For small lists, the computation can be faster for a simple list, although extremely fast in both cases. When the size of the list increases, upward to a million, summing all the items has computational issues, whereas C1LengthList maintains steady performance.

## Conclusion<span class="css-1afrefi" aria-label="Copy"></span>

This hierarchical data structure allows us to efficiently get the offset of an item in the list in o(log(n)), instead of o(n) of a traditional approach using a list. The only major con is that the order to access the length of a specific item is also o(log(n)) instead of o(1). Also, this data structure allows us to drastically reduce the memory used for lists whose items are mostly equal to a default value.

Another use case scenario to consider is the calculation of a UI scrolling-index of cells that handle text-wrapping. Text-wrapping easily transmits the idea of an unknown height of the cell caused by the inner text glyph height + the width of the view-port, all to affect the positions of all the following rows. In such a case scenario, the precise index of the scroll is a challenge.

You can try C1LengthList as part of [C1DataCollection](/componentone/net-data-service-components/data-collection-view "/componentone/net-data-service-components/data-collection-view") and [ComponentOne WPF Edition](/componentone/wpf-ui-controls "/componentone/wpf-ui-controls").