Socially Distant OS
  • Docs
  • API
Search Results for

    Show / Hide Table of Contents
    • Accessibility
      • In-Game TTS
    • Development and Modding: Getting started
      • Building from source
      • Contribution guidelines
      • Project structure
      • Code style guide
    • Game scripting (sdsh)
      • The Basics
    • Game Framework
      • Event Bus
      • Playing Audio
    • Story Scripting
      • Career Mode Narrative Objects
      • Narrative Identifiers
      • News Articles
    • User Interface
      • UI Overview
      • Signals
      • List Adapters
      • Optimizing UI for Performance
      • Advanced UI features
        • Visual Styles

    List Adapters

    A ListAdapter is a tool in the game's UI system that allows you to display a list of widgets based on a common data model. For example, the System Settings category list and the settings themselves are both ListAdapters.

    You can use a ListAdapter to display any kind of data inside any kind of widget deriving from ContainerWidget. If you are familiar with Optimized ScrollView Adapter, the API is very similar. However, a ListAdapter doesn't need to be a scroll view.

    Creating your own ListAdapter

    Let's create a ListAdapter that can display a list of cracked user credentials.

    First, create the data model itself

    public struct CrackedPassword
    {
        public string FullName;
        public string UserName;
        public string Password;
    }
    

    And its list - it can be anything implementing Ienumerable<CrackedPassword>:

    var data = new CrackedPassword[] {
        new CrackedPassword { FullName = "Brodie Robertson", UserName = "brodieonlinux", Password = "wayland shill" },
        new CrackedPassword { FullName = "Ritchie Frodomar", UserName = "ritchie", Password = "strong and complicated password" },
        new CrackedPassword { FullName = "TheEvilSkeleton", UserName = "tesk", Password = "bottleofwine" }
    };
    

    Next, create a Widget class that can display a CrackedPassword:

    public sealed class CrackedPasswordView : Widget 
    {
        private readonly StackPanel stack = new StackPanel();
        private readonly TextWidget fullName = new TextWidget();
        private readonly TextWidget username = new TextWidget();
        private readonly TextWidget password = new TextWidget();
        
        public CrackedPasswordView()
        {
            Children.Add(stack);
        
            stack.ChildWidgets.Add(fullName);
            stack.ChildWidgets.Add(username);
            stack.ChildWidgets.Add(password);
        }
        
        public void UpdateView(CrackedPassword data)
        {
            fullName.Text = data.FullName;
            username.Text = data.UserName;
            password.Text = data.Password;
        }
    }
    

    The important part is the UpdateModel(CrackedPassword) method - this is what you will call when it's time to update a widget with new data.

    Now that we've defined the widget, it's time to define its ViewHolder. A ViewHolder acts as the "slot" for the widget type we just created, within the ListAdapter. There will be a ViewHolder for each element in the data source.

    public sealed class CrackedPasswordViewHolder : ViewHolder
    {
        private readonly CrackedPasswordView view = new();
        
        public CrackedPasswordViewHolder(int itemIndex, Box root) : base(itemIndex, root)
        {
            root.Content = view;
        }
        
        public void UpdateView(CrackedPassword data)
        {
            view.UpdateView(data);    
        }
    }
    

    Most ViewHolder objects will look identical to the code above. All a ViewHolder must do is create an instance of the view widget, assign it as the Content of a "root" widget, and expose any API of the view widget needed by your ListAdapter class.

    We now have everything we need to create the ListAdapter itself. We will display our data inside a StackPanel.

    public sealed class CrackedPasswordList : ListAdapter<StackPanel, CrackedPasswordViewHolder>
    {
        
    }
    

    The first type parameter of ListAdapter declares the type of ContainerWidget we want to display all of our list items in, in this case StackPanel. The second parameter declares the ViewHolder-deriving type we just created.

    We must override two abstract methods to tell the ListAdapter how to interact with our data - TViewHolder CreateViewHolder(int, Box) and void pdateViewHolder(TViewHolder).

    The CreateViewHolder method just constructs a new ViewHolder instance of the required type and returns it. This method is called when a new view widget needs to be created. We are given the item index and root widget of the new view, so we can just pass them to the constructor of CrackedPasswordViewHolder.

    public override CrackedPasswordViewHolder CreateViewHolder(int itemIndex, Box rootWidget)
    {
        return new CrackedPasswordViewHolder(itemIndex, rootWidget);
    }
    

    UpdateViewHolder() is called every time the data of a list item has changed. This method receives the item's view holder, and must retrieve the new data and update the view with it.

    protected override void UpdateViewHolder(CrackedPasswordViewHolder viewHolder)
    {
        CrackedPassword item = items[viewHolder.ItemIndex];
        viewHolder.UpdateView(item);
    }
    

    The only problem we need to solve now is telling ListAdapter when our data changes, and being able to access it. This is what the DataHelper<T> class is for.

    In the CrackedPasswordListAdapter, add a readonly field items of type DataHelper<CrackedPassword> and construct it.

    private readonly DataHelper<CrackedPassword> items;
    
    public CrackedPasswordList()
    {
        items = new DataHelper<CrackedPassword>(this);   
    }
    

    We can now populate the ListAdapter with data by calling items.SetItems().

    var data = new CrackedPassword[] {
        new CrackedPassword { FullName = "Brodie Robertson", UserName = "brodieonlinux", Password = "wayland shill" },
        new CrackedPassword { FullName = "Ritchie Frodomar", UserName = "ritchie", Password = "strong and complicated password" },
        new CrackedPassword { FullName = "TheEvilSkeleton", UserName = "tesk", Password = "bottleofwine" }
    };
    
    items.SetItems(data);
    

    Examples of ListAdapter used in-game

    System Settings

    • the sidebar listing all settings categories
    • the scroll view of the active category, where all of its settings widgets are shown

    Desktop

    • the Info Panel (mission objectives and notifications)
    • the Dock (icon groups)
    • the Application Launcher (the grid view of icons)

    That's by no means exhaustive, ListAdapter is used everywhere.

    Performance concerns

    ListAdapter tries its best to keep performance issues at bay, but it needs to work with what it's given. You should keep a few things in mind.

    Widget recycling

    When items are removed from a ListADapter, their view widgets are recycled. When new items are added to the list adapter, widgets are pulled from the recycle bin first. However, this does not extend to the child of the view widget. This is because ListAdapter can't guess how your custom widget handles state or memory.

    If you'd like to allow ListADapter to recycle your custom widget, you will need to do some extra work.

    Inside your ViewHolder constructor, replace:

    view = new CrackedPasswordView();
    
    // WITH
    
    view = RecycleBin.Get<CrackedPasswordView>().GetWidget();
    

    This allows you to retrieve your CrackedPasswordView instance from the recycle bin. If there are no recyclable instances, a new instance will be created. This means you can only use the recycle bin if your widget has a public parameterless constructor.

    Next, inside your ViewHolder class, add:

    public void Recycle()
    {
        // NOTE: You cannot recycle a toplevel widget OR a widget that's added as a child to another.
        Root.Content = null;
        RecycleBin.Get<CrackedPasswordView>().Recycle(view);   
    }
    

    This method puts your CrackedPasswordView instance in the recycle bin. You call this method when a list item is being removed from a ListAdapter. To get that working, override the following method in your ListAdapter class.

    protected override void BeforeRemoveItem(CrackedPasswordViewHolder holder)
    {
        holder.Recycle();
    }
    

    ⚠️ Warning

    Before recycling a widget, make sure you remove it from its parent and unbind all event/callback delegates. You do not need to release any unmanaged resources, since the idea is to re-use them when the widget itself is re-used. Recycling is not the same as disposal.

    *️⃣ Note

    The Recycle Bin is a shared resource. When calling RecycleBin.Get<T>(), you are retrieving the shared recycle bin for that exact type of widget. You are not retrieving a new recycle bin instance unless that widget type has never been recycled before.

    Layout and geometry updates

    When data in a ListAdapter changes, so too shall its widget layout and geometry. Only notify ListAdapter of data changes when you know the data has changed.

    In this article
    Back to top Generated by DocFX