Back to Devexpress

Save and Restore Layout

blazor-405458-components-pivottable-save-and-restore-layout.md

latest20.7 KB
Original Source

Save and Restore Layout

  • May 23, 2025
  • 9 minutes to read

The Blazor Pivot Table allows you to save and restore its layout. Use this functionality to implement the following scenarios:

  • Persist layout between application runs to retain users’ changes.
  • Allow users to save their preferred layouts and return to them when necessary.
  • Configure pre-defined layouts and allow users to switch between them.

A saved layout object (PivotTablePersistentLayout) includes the following data:

Saved informationPivotTablePersistentLayout’s property
Expand/collapse state for rows and columnsLayout.ExpandCollapseState
Filter values. All Pivot Table field filter criteria[1] combined by logical AND.Layout.FilterCriteria
Settings for individual fields. See the table below.Layout.Fields.Item(i)

The PivotTablePersistentLayout.Fields collection stores information about field settings. Each collection item (PivotTablePersistentLayoutField object) includes the following data:

Saved informationPivot Table field parameterPivotTablePersistentLayoutField’s property
Data source field nameDxPivotTableField.FieldLayoutField.Field
AreaDxPivotTableField.AreaLayoutField.Area
Field index within its areaDxPivotTableField.AreaIndexLayoutField.AreaIndex
Sort directionDxPivotTableField.SortOrderLayoutField.SortOrder

Persist Layout Between Application Runs

To save and restore the Pivot Table layout automatically, handle the following events:

LayoutAutoSavingFires each time the Pivot Table’s layout changes and allows you to save the layout.LayoutAutoLoadingFires once the Pivot Table component is initialized and allows you to restore the saved layout.

The following code snippet demonstrates how to implement layout persistence for a Pivot Table. When layout changes, the LayoutAutoSaving event handler saves the updated layout to the browser’s local storage. Once the page is reloaded or restored, the LayoutAutoLoading event handler loads the most recently saved layout from the local storage and applies it to the Pivot Table.

razor
@using System.Text.Json
@inject NwindDataService NwindDataService
@inject IJSRuntime JSRuntime
<DxPivotTable @ref="@PivotTable"
              Data="@PivotData"
              VirtualScrollingEnabled="true"
              LayoutAutoLoading="PivotTable_LayoutAutoLoading"
              LayoutAutoSaving="PivotTable_LayoutAutoSaving">
    <Fields>
        <DxPivotTableField Field="SalesPerson" Area="PivotTableArea.Row" Caption="Sales Person" Width="270"/>
        <DxPivotTableField Field="ProductName" Area="PivotTableArea.Row" Caption="Product Name"/>
        <DxPivotTableField Field="ProductAmount" Area="PivotTableArea.Data" Caption="Product Amount" CellFormat="{0:c0}"/>
        <DxPivotTableField Field="Discount" Area="PivotTableArea.Data" CellFormat="{0:p0}"/>
        <DxPivotTableField Field="Country" AreaIndex="1"/>
        <DxPivotTableField Field="Region" AreaIndex="0"/>
        <DxPivotTableField Field="City" AreaIndex="-1"/>
        <DxPivotTableField Field="ExtendedPrice" AreaIndex="-1"/>
        <DxPivotTableField Field="OrderDate" GroupInterval="PivotTableGroupInterval.DateYear" Area="PivotTableArea.Column" Caption="Order Year" />
        <DxPivotTableField Field="OrderDate" GroupInterval="PivotTableGroupInterval.DateQuarter" Area="PivotTableArea.Column" Caption="Order Quater">
            <ValueTemplate>
                <span>@($"Q{context.Text}")</span>
            </ValueTemplate>
        </DxPivotTableField>
    </Fields>
</DxPivotTable>

@code {
    const string LocalStorageKey = "PivotTable-LayoutPersistence-Data";
    object PivotData { get; set; }
    IPivotTable PivotTable { get; set; }
    bool PreRendered { get; set; }
    protected override void OnAfterRender(bool firstRender) {
        if(firstRender) {
            PreRendered = true;
            StateHasChanged();
        }
    }
    protected override async Task OnInitializedAsync() {
        var invoices = await NwindDataService.GetInvoicesAsync();
        var customers = await NwindDataService.GetCustomersAsync();
        var minDate = invoices.Min(i => i.OrderDate).GetValueOrDefault();
        var maxDate = invoices.Max(i => i.OrderDate).GetValueOrDefault();
        PivotData = invoices.Where(i => {
            var invoiceDate = i.OrderDate.GetValueOrDefault();
            return invoiceDate.Year > minDate.Year && invoiceDate.Year <= maxDate.Year;
        }).Join(customers, i => i.CustomerId, c => c.CustomerId, (i, c) => {
            return new {
                CompanyName = c.CompanyName,
                UnitPrice = i.UnitPrice,
                OrderDate = i.OrderDate,
                ProductName = i.ProductName,
                ProductAmount = i.Quantity * i.UnitPrice,
                Country = c.Country,
                Region = c.Region,
                ExtendedPrice = i.ExtendedPrice,
                City = c.City,
                Discount = i.Discount,
                SalesPerson = i.Salesperson,
            };
        });
    }
    async Task PivotTable_LayoutAutoLoading(PivotTablePersistentLayoutEventArgs e) {
        e.Layout = await LoadLayoutFromLocalStorageAsync();
    }
    async Task PivotTable_LayoutAutoSaving(PivotTablePersistentLayoutEventArgs e) {
        await SaveLayoutToLocalStorageAsync(e.Layout);
    }
    // Refer to https://docs.microsoft.com/en-us/aspnet/core/blazor/state-management
    // to learn more about Blazor state management
    // In Blazor Server apps, prefer ASP.NET Core Protected Browser Storage
    async Task<PivotTablePersistentLayout> LoadLayoutFromLocalStorageAsync() {
        try {
            var json = await JSRuntime.InvokeAsync<string>("localStorage.getItem", LocalStorageKey);
            return JsonSerializer.Deserialize<PivotTablePersistentLayout>(json);
        } catch {
            // Mute exceptions for the server prerender stage
            return null;
        }
    }
    async Task SaveLayoutToLocalStorageAsync(PivotTablePersistentLayout layout) {
        try {
            var json = JsonSerializer.Serialize(layout);
            await JSRuntime.InvokeVoidAsync("localStorage.setItem", LocalStorageKey, json);
        } catch {
            // Mute exceptions for the server prerender stage
        }
    }
    async Task RemoveLayoutFromLocalStorageAsync() {
        try {
            await JSRuntime.InvokeVoidAsync("localStorage.removeItem", LocalStorageKey);
        } catch {
            // Mute exceptions for the server prerender stage
        }
    }
    async Task ReloadPageButton_ClickAsync() {
        await JSRuntime.InvokeVoidAsync("location.reload");
    }
    async Task ResetLayoutButton_ClickAsync() {
        await RemoveLayoutFromLocalStorageAsync();
        await JSRuntime.InvokeVoidAsync("location.reload");
    }
    void FieldListButton_Click() {
        PivotTable.ShowFieldList();
    }
}
csharp
using System;
using System.Collections.Generic;
using System.Runtime.InteropServices;

namespace BlazorDemo.Data.Northwind {
    public class Invoice {
        public string ShipName { get; set; }
        public string ShipAddress { get; set; }
        public string ShipCity { get; set; }
        public string ShipRegion { get; set; }
        public string ShipPostalCode { get; set; }
        public string ShipCountry { get; set; }
        public string CustomerId { get; set; }
        public string CustomerName { get; set; }
        public string Address { get; set; }
        public string City { get; set; }
        public string Region { get; set; }
        public string PostalCode { get; set; }
        public string Country { get; set; }
        public string Salesperson { get; set; }
        public int OrderId { get; set; }
        public DateTime? OrderDate { get; set; }
        public DateTime? RequiredDate { get; set; }
        public DateTime? ShippedDate { get; set; }
        public string ShipperName { get; set; }
        public int ProductId { get; set; }
        public string ProductName { get; set; }
        public decimal UnitPrice { get; set; }
        public short Quantity { get; set; }
        public float Discount { get; set; }
        public decimal? ExtendedPrice { get; set; }
        public decimal? Freight { get; set; }
    }
}
csharp
using System;
using System.Collections.Generic;
using System.Runtime.InteropServices;

namespace BlazorDemo.Data.Northwind {
    public partial class Customer {
        public Customer() {
            Orders = new HashSet<Order>();
        }

        public string CustomerId { get; set; }
        public string CompanyName { get; set; }
        public string ContactName { get; set; }
        public string ContactTitle { get; set; }
        public string Address { get; set; }
        public string City { get; set; }
        public string Region { get; set; }
        public string PostalCode { get; set; }
        public string Country { get; set; }
        public string Phone { get; set; }
        public string Fax { get; set; }

        public virtual ICollection<Order> Orders { get; set; }
    }
}
csharp
using System.Collections.Generic;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using BlazorDemo.Data.Northwind;
using BlazorDemo.DataProviders;

namespace BlazorDemo.Services {
    public partial class NwindDataService {
        public Task<IEnumerable<Invoice>> GetInvoicesAsync(CancellationToken ct = default) {
            // Return your data here
        }
    }
}
csharp
using System.Collections.Generic;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using BlazorDemo.Data.Northwind;
using BlazorDemo.DataProviders;

namespace BlazorDemo.Services {
    public partial class NwindDataService {
        public Task<IEnumerable<Customer>> GetCustomersAsync(CancellationToken ct = default) {
            // Return your data here
        }
    }
}
csharp
// ...
builder.Services.AddScoped<NwindDataService>();

Run Demo: Save and Restore the Layout

Save and Restore the Layout on Demand

To save and restore the Pivot Table layout when necessary (for example, on a button click), call the following methods:

SaveLayout()Returns Pivot Table layout information so you can save it to a variable or a custom storage.LoadLayout(PivotTablePersistentLayout)Restores Pivot Table layout.

The following code snippet displays two buttons: Save Layout and Load Layout. When a user clicks the first button, the current Pivot Table layout is saved to the Layout variable. Once a user clicks the second button, the component loads the most recently saved layout and applies it to the Pivot Table.

razor
@rendermode InteractiveServer
@using Services

<DxButton Text="Save Layout" Click="OnSaveClick" />
<DxButton Text="Load Layout" Click="OnLoadClick" />

<DxPivotTable Data="SalesData" @ref=MyPivotTable >
    <Fields>
        <DxPivotTableField Field="@nameof(Sales.SaleInfo.Region)"
                           Area="@PivotTableArea.Row"
                           AreaIndex="0" />
        <DxPivotTableField Field="@nameof(Sales.SaleInfo.Country)"
                           Area="@PivotTableArea.Row"
                           SortOrder="@PivotTableSortOrder.Descending"
                           AreaIndex="1" />
        <DxPivotTableField Field="@nameof(Sales.SaleInfo.Date)"
                           GroupInterval="@PivotTableGroupInterval.DateYear"
                           Area="@PivotTableArea.Column"
                           AreaIndex="0"
                           Caption="Year" />
        <DxPivotTableField Field="@nameof(Sales.SaleInfo.Date)"
                           GroupInterval="@PivotTableGroupInterval.DateQuarter"
                           Area="@PivotTableArea.Column"
                           AreaIndex="1"
                           Caption="Quarter" />
        <DxPivotTableField Field="@nameof(Sales.SaleInfo.Amount)"
                           SortOrder="@PivotTableSortOrder.Ascending"
                           Area="@PivotTableArea.Data"
                           SummaryType="@PivotTableSummaryType.Sum" />
        <DxPivotTableField Field="@nameof(Sales.SaleInfo.City)"
                           Area="@PivotTableArea.Filter"/>
    </Fields>
</DxPivotTable>

@code {
    IPivotTable MyPivotTable { get; set; }
    PivotTablePersistentLayout Layout { get; set; }
    IEnumerable<Sales.SaleInfo> SalesData;
    protected override async Task OnInitializedAsync() {
        SalesData = await Sales.GetSalesAsync();
    }
    void OnSaveClick() {
        Layout = MyPivotTable.SaveLayout();
    }
    void OnLoadClick() {
        MyPivotTable.LoadLayout(Layout);
    }
}
csharp
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;

public class Sales {
    static IList<SaleInfo> dataSource;

    public class SaleInfo {
        public int OrderId { get; set; }
        public string Region { get; set; }
        public string Country { get; set; }
        public string City { get; set; }
        public int Amount { get; set; }
        public DateTime Date { get; set; }
    }

    static Sales() {
        CreateDataSource();
    }

    public static Task<IQueryable<SaleInfo>> GetSalesAsync() {
        return Task.FromResult(dataSource.AsQueryable());
    }

    static void CreateDataSource() {
        dataSource = new List<SaleInfo> {
        new SaleInfo {
            OrderId = 10248,
            Region = "North America",
            Country = "United States",
            City = "New York",
            Amount = 1740,
            Date = DateTime.Parse("2017/01/06")
        },
        new SaleInfo {
            OrderId = 10249,
            Region = "North America",
            Country = "United States",
            City = "Los Angeles",
            Amount = 850,
            Date = DateTime.Parse("2017/02/13")
        },
        new SaleInfo {
            OrderId = 10250,
            Region = "North America",
            Country = "United States",
            City = "Denver",
            Amount = 2235,
            Date = DateTime.Parse("2017/02/07")
        },
        new SaleInfo {
            OrderId = 10251,
            Region = "North America",
            Country = "Canada",
            City = "Vancouver",
            Amount = 1965,
            Date = DateTime.Parse("2017/03/03")
        },
        new SaleInfo {
            OrderId = 10252,
            Region = "North America",
            Country = "Canada",
            City = "Edmonton",
            Amount = 880,
            Date = DateTime.Parse("2017/03/10")
        },
        new SaleInfo {
            OrderId = 10253,
            Region = "South America",
            Country = "Brazil",
            City = "Rio de Janeiro",
            Amount = 5260,
            Date = DateTime.Parse("2017/01/17")
        },
        new SaleInfo {
            OrderId = 10254,
            Region = "South America",
            Country = "Argentina",
            City = "Buenos Aires",
            Amount = 2790,
            Date = DateTime.Parse("2017/05/21")
        },
        new SaleInfo {
            OrderId = 10255,
            Region = "South America",
            Country = "Paraguay",
            City = "Asuncion",
            Amount = 3140,
            Date = DateTime.Parse("2017/05/01")
        },
        new SaleInfo {
            OrderId = 10256,
            Region = "Europe",
            Country = "United Kingdom",
            City = "London",
            Amount = 6175,
            Date = DateTime.Parse("2017/06/24")
        },
        new SaleInfo {
            OrderId = 10257,
            Region = "Europe",
            Country = "Germany",
            City = "Berlin",
            Amount = 4575,
            Date = DateTime.Parse("2017/06/11")
        },
        new SaleInfo {
            OrderId = 10517,
            Region = "North America",
            Country = "United States",
            City = "New York",
            Amount = 7710,
            Date = DateTime.Parse("2018/07/18")
        },
        new SaleInfo {
            OrderId = 10518,
            Region = "North America",
            Country = "United States",
            City = "Los Angeles",
            Amount = 7975,
            Date = DateTime.Parse("2018/01/10")
        },
        new SaleInfo {
            OrderId = 10519,
            Region = "North America",
            Country = "United States",
            City = "Denver",
            Amount = 3285,
            Date = DateTime.Parse("2018/03/13")
        },
        new SaleInfo {
            OrderId = 10520,
            Region = "North America",
            Country = "Canada",
            City = "Vancouver",
            Amount = 2580,
            Date = DateTime.Parse("2018/04/22")
        },
        new SaleInfo {
            OrderId = 10521,
            Region = "North America",
            Country = "Canada",
            City = "Edmonton",
            Amount = 2160,
            Date = DateTime.Parse("2018/05/26")
        },
        new SaleInfo {
            OrderId = 10522,
            Region = "South America",
            Country = "Brazil",
            City = "Rio de Janeiro",
            Amount = 1100,
            Date = DateTime.Parse("2018/05/25")
        },
        new SaleInfo {
            OrderId = 10523,
            Region = "South America",
            Country = "Argentina",
            City = "Buenos Aires",
            Amount = 4425,
            Date = DateTime.Parse("2018/08/21")
        },
        new SaleInfo {
            OrderId = 10524,
            Region = "South America",
            Country = "Paraguay",
            City = "Asuncion",
            Amount = 1360,
            Date = DateTime.Parse("2018/08/22")
        },
        new SaleInfo {
            OrderId = 10525,
            Region = "Europe",
            Country = "United Kingdom",
            City = "London",
            Amount = 3250,
            Date = DateTime.Parse("2018/11/14")
        },
        new SaleInfo {
            OrderId = 10526,
            Region = "Europe",
            Country = "Germany",
            City = "Berlin",
            Amount = 5550,
            Date = DateTime.Parse("2018/12/21")
        },
    };
}

Run Demo: Save and Restore the Layout

Footnotes

  1. DevExpress components can incorrectly serialize custom enumeration values in criteria operators. Refer to the following troubleshooting topic for additional information: The XXX enumeration type is not registered for the parse operation…