How UI Virtualization Works in a C# .NET Datagrid
A WPF user interface is created by adding UI elements to the visual tree. A WPF datagrid control is composed of many, many UI elements. For example, each cell in a datagrid may contain at least two primitive elements like a TextBlock and Border. Now imagine when the datagrid displays thousands of rows, a lot of UI elements are required, and they each take up a finite amount of memory. In this blog, we will explain how UI Virtualization solves this problem and how we implement it for our WPF datagrid control.
What is UI Virtualization
When a UI control needs to display a large number of items, it becomes necessary to optimize the visual tree to consume less memory and maintain good application performance. A common solution is known as UI Virtualization, where the items are created and disposed of as the user scrolls or navigates around the interface.
A common term to understand is the Viewport. This refers to the visual area of a control, representing the minimum number of UI elements required without compromising the desired UI.
Elements within the viewport are created, updated, or destroyed during the layout pass.
In smaller or static controls, all the visual elements are created at once, and the successive calls to the layout of the control are simply to reposition the elements according to the layout dimensions passed by the parent. This makes the layout very light and responsive, but in UI-virtualizing controls, the creation of the elements is delayed and delegated to the layout pass; this is because it is necessary to know the dimensions of the control to create the visible children according to those dimensions.
Since creating and laying out the children can be a heavy task, and everything needs to be performed in the layout pass, it is very important for this piece of code to be very fast. Otherwise, the UI will behave clunky when scrolling. Making this code fast is very dependent on the control. Still, some general techniques will help, like recycling the visual elements, reducing the visual tree as much as possible, and optimizing the layout algorithm itself.
How UI Virtualization Works by Recycling
In UI virtualizing controls that render a series of items, like listview, datagrids, treeviews, etc., when one item goes out of view, that element can be used to render a new appearing element. This is known as recycling. This way, the creation of the element is dodged, and it only needs to be bound to the new data item, a visual element preserving the UI. That is a big save in terms of performance because the process of destroying and creating the element is skipped. It saves just a small amount of memory and time, but with a large datagrid, that small saving becomes more significant.
The simplest way is to recycle all the elements and bind new displaying items to the recycled elements in every layout pass, but this can be improved to detect whether some items were already laid out in the previous pass. Since those items are already bound, recalculating the position is enough; this way, we double save in the creation of the visual element as well as binding the item to the visual element.
How to Recycle Different Elements
Recycling is straightforward when all the elements follow the same template or type. In some cases, the visual elements to be shown are not the same kind, for instance, a list-view with an item with a text inside and another item with an image or a checkbox. This creates a problem for the recycling algorithm since a recycled element could not be useful for another item. Needing to recreate part of the visual tree will not be good. The most effective technique is to detect the different kinds of visual elements needed according to the items and recycle them in different buckets. This way, once an element is recycled, the visual element is just like needed, and no changes in the visual tree are performed.
Optimizing the Layout Pass
In most platforms, the layout pass consists of two steps: measuring and arranging. The less code these steps execute, the faster the layout pass will be. Measuring elements is an expensive task and should be avoided as much as possible. In a datagrid, we typically do NOT measure the text length and adjust the cell because this is an expensive task, and normally, each cell is the same size. However, some other scenarios may require the element size to be measured to determine the position, and you have to accept the expensive task.
During the arrange phase, the main task is to position the elements. Therefore, the goal is to complete the position quickly and efficiently. In our previous article, we wrote about the data structure suitable for quickly computing complex and big layout positions.
Another way to make the layout lighter is to reduce the layout of every item. Let’s consider a typical scenario with a border element with text inside, like a listview item or datagrid cell. The natural solution for this is to place a TextBlock element inside the Border element. But these UI elements do more than just display lines and text, which adds overhead to the functionality. For instance, the TextBlock is a deeply rich element that does a lot, but there are faster solutions if you only need to draw text on the screen. 2D rendering with raw graphics drawing methods like DrawText can be used to paint the text faster and gain performance enhancements in the long run. We implemented this approach to quickly draw datagrid cells in the ComponentOne FlexGrid for WPF. You can find more about this technique in our previous blog: How to Improve WPF Datagrid Performance with 2D Cell Rendering | ComponentOne.
Ready to Get Started? Download ComponentOne WPF Edition!
Solving Problems of the Layout Pass with Render Scrolling
When a UI control is scrolled, this will initiate the layout pass. As you scroll down a listview or datagrid, the layout pass will be initiated too many times (once per every pixel change), so performing the measure and arranging processes that frequently will hurt your performance. If you’re calculating an item offset during each measuring process, that will add to the complexity. This will be OK in some scenarios, like mouse wheel scrolling, but not for drag gestures with inertia. We can overcome this problem with something that we're calling render scrolling.
The result of each layout pass is rendered to the GPU. After this point, we can continue to arrange and modify the appearance of the UI elements using properties like opacity, scaling, translating, etc. These properties do not affect the layout but just the appearance of the rendered elements, even if you perform a scale or translate transform. By using these properties to further move and scale elements, we can achieve the desired result without impacting the layout pass at all. The result ends up being blazingly fast for scrolling since the operation is performed directly on the GPU.
The animations below illustrate the difference between standard layout scroll updating and our render scrolling. The green flickers represent the frequency of updates.
Layout Scrolling Render Scrolling
When using this “render scrolling” approach, it will still be necessary to invalidate (or update) the layout at certain times. Otherwise, we would end up seeing a blank screen. The pace of invalidating the layout is much less than using standard layout pass-based scrolling. To avoid a blank screen during scroll, it’s often necessary to layout some extra items just outside the viewport so they are already loaded and ready before appearing on screen. This is necessary since the render scrolling is always faster than the normal layout updates, but that viewport gap cannot be too big. Otherwise, we would affect the layout, making it compute more elements.
Conclusion
This blog shared some tips and techniques for handling different UI virtualization solutions. Microsoft does support recycling in its ItemsControls, and of course, if you’re using a complete WPF datagrid like C1FlexGrid, you do not have to worry about implementing any of the features we discussed above because we’ve already included it as built-in functionality. You can download the WPF FlexGrid from the link below or on NuGet (C1.WPF.FlexGrid).
Ready to Get Started? Download ComponentOne WPF Edition!