New to Telerik UI for Blazor? Download free 30-day trial

Blazor Grid CRUD Operations

The Blazor Grid supports CRUD operations and validation. Use the CRUD events to transfer the changes to the underlying data source (for example, call a service to update the database, and not only with the view data).

This page explains how to enable editing, use the relevant events and command buttons. There is also a runnable code example.

In this article:

Basics

The Grid offers several editing modes with different user experience. Set the EditMode property to a member of the GridEditMode enum:

Events

  • OnAdd - fires when the Add command button for a newly added item is clicked. The event is cancellable.
  • OnCreate - fires when the Save command button for a newly added item is clicked. Cancellable (cancelling it keeps the grid in Insert mode).
  • OnUpdate - fires when the Save command button is clicked on an existing item. Cancellable (cancelling it keeps the grid in Edit mode). The model reference is a copy of the original data source item.
  • OnDelete - fires when the Delete command button is clicked. You can also display a delete confirmation dialog before the deletion.
  • OnEdit - fires when the user is about to enter edit mode for an existing row. Cancellable (cancelling it prevents the item from opening for editing).
  • OnCancel - fires when the user clicks the Cancel command button. Allows you to undo the changes to the data in the view data. Cancellable (keeps the grid in Edit/Insert mode).
  • OnRead - fires when the grid needs data - after any data source operation like updating, creating, deleting, filtering, sorting. If you cancel the CUD events, the OnRead event will not fire.

The CUD event handlers receive an argument of type GridCommandEventArgs that exposes the following fields:

  • IsCancelled - a boolean field indicating whether the grid operation is to be prevented (for example, prevent a row from opening for edit, or from updating the data layer).
  • IsNew - a boolean field indicating whether the item was just added through the grid. Lets you differentiate a data source Create operation from Update operation in the OnClick event of a command button.
  • Item - an object you can cast to your model class to obtain the current data item.
  • Field - specific to InCell editing - indicates which is the model field the user changed when updating data.
  • Value - specific to InCell editing - indicates what is the new value the user changed when updating data.

You can initiate editing or inserting of an item from anywhere on the page (buttons outside of the grid, or components in a column template) through the grid state.

Customize The Editor Fields

You can customize the editors rendered in the Grid by providing the EditorType attribute, exposed on the <GridColumn>, or by using the Editor Template. The EditorType attribute accepts a member of the GridEditorType enum:

Field data type GridEditorType enum members
Text GridEditorType.TextArea
GridEditorType.TextBox
Boolean GridEditorType.CheckBox
GridEditorType.Switch
DateTime GridEditorType.DatePicker
GridEditorType.DateTimePicker
GridEditorType.TimePicker
@* The usage of the EditorType parameter *@

@using System.ComponentModel.DataAnnotations

<TelerikGrid Data=@MyData 
             EditMode="@GridEditMode.Inline" 
             Pageable="true" 
             Height="400px"
             OnUpdate="@UpdateHandler" 
             OnDelete="@DeleteHandler" 
             OnCreate="@CreateHandler" 
             OnCancel="@CancelHandler">
    <GridToolBarTemplate>
        <GridCommandButton Command="Add" Icon="@SvgIcon.Plus">Add Employee</GridCommandButton>
    </GridToolBarTemplate>
    <GridColumns>
        <GridColumn Field=@nameof(SampleData.ID) Title="ID" Editable="false" />
        <GridColumn Field=@nameof(SampleData.Name) 
                    EditorType="@GridEditorType.TextArea"
                    Title="Name" />
        <GridCommandColumn>
            <GridCommandButton Command="Save" Icon="@SvgIcon.Save" ShowInEdit="true">Save</GridCommandButton>
            <GridCommandButton Command="Edit" Icon="@SvgIcon.Pencil">Edit</GridCommandButton>
            <GridCommandButton Command="Delete" Icon="@SvgIcon.Trash">Delete</GridCommandButton>
            <GridCommandButton Command="Cancel" Icon="@SvgIcon.Cancel" ShowInEdit="true">Cancel</GridCommandButton>
        </GridCommandColumn>
    </GridColumns>
</TelerikGrid>


@code {
    async Task UpdateHandler(GridCommandEventArgs args)
    {
        SampleData item = (SampleData)args.Item;

        await MyService.Update(item);

        await GetGridData();
    }

    async Task DeleteHandler(GridCommandEventArgs args)
    {
        SampleData item = (SampleData)args.Item;

        await MyService.Delete(item);

        await GetGridData();
    }

    async Task CreateHandler(GridCommandEventArgs args)
    {
        SampleData item = (SampleData)args.Item;

        await MyService.Create(item);

        await GetGridData();
    }

    async Task CancelHandler(GridCommandEventArgs args)
    {
        SampleData item = (SampleData)args.Item;

        await Task.Delay(1000); //simulate actual long running async operation
    }

    public class SampleData
    {
        public int ID { get; set; }
        [Required]
        public string Name { get; set; }
    }

    List<SampleData> MyData { get; set; }

    async Task GetGridData()
    {
        MyData = await MyService.Read();
    }

    protected override async Task OnInitializedAsync()
    {
        await GetGridData();
    }

    public static class MyService
    {
        private static List<SampleData> _data { get; set; } = new List<SampleData>();

        public static async Task Create(SampleData itemToInsert)
        {
            await Task.Delay(1000); // simulate actual long running async operation

            itemToInsert.ID = _data.Count + 1;
            _data.Insert(0, itemToInsert);
        }

        public static async Task<List<SampleData>> Read()
        {
            await Task.Delay(1000); // simulate actual long running async operation

            if (_data.Count < 1)
            {
                for (int i = 1; i < 50; i++)
                {
                    _data.Add(new SampleData()
                    {
                        ID = i,
                        Name = "Name " + i.ToString()
                    });
                }
            }

            return await Task.FromResult(_data);
        }

        public static async Task Update(SampleData itemToUpdate)
        {
            await Task.Delay(1000); // simulate actual long running async operation

            var index = _data.FindIndex(i => i.ID == itemToUpdate.ID);
            if (index != -1)
            {
                _data[index] = itemToUpdate;
            }
        }

        public static async Task Delete(SampleData itemToDelete)
        {
            await Task.Delay(1000); // simulate actual long running async operation

            _data.Remove(itemToDelete);
        }
    }
}

Example

The example below shows how you can handle the events the grid exposes, so you can Create, Update or Delete records in your data source and the view model.

You can see the CRUD events in action in our live demos for Inline, Popup and InCell editing. You can also use the Telerik Wizard project templates to easily create an application that showcases the Telerik Grid with CRUD events implemented.

Handling the CRUD events of the grid to save data to the actual data source (mocked with local methods in this example, see the code comments for details)

@using System.ComponentModel.DataAnnotations @* for the validation attributes *@ 

Editing is cancelled for the first two records.
<br />
<strong>There is a deliberate delay</strong> in the data source operations in this example to mimic real life delays and to showcase the async nature of the calls.

<TelerikGrid Data=@MyData EditMode="@GridEditMode.Inline" Pageable="true" Height="400px"
             OnAdd="@AddHandler" OnUpdate="@UpdateHandler" OnEdit="@EditHandler" OnDelete="@DeleteHandler" OnCreate="@CreateHandler" OnCancel="@CancelHandler">
    <GridToolBarTemplate>
        <GridCommandButton Command="Add" Icon="@SvgIcon.Plus">Add Employee</GridCommandButton>
    </GridToolBarTemplate>
    <GridColumns>
        <GridColumn Field=@nameof(SampleData.ID) Title="ID" Editable="false" />
        <GridColumn Field=@nameof(SampleData.Name) Title="Name" />
        <GridCommandColumn>
            <GridCommandButton Command="Save" Icon="@SvgIcon.Save" ShowInEdit="true">Save</GridCommandButton>
            <GridCommandButton Command="Edit" Icon="@SvgIcon.Pencil">Edit</GridCommandButton>
            <GridCommandButton Command="Delete" Icon="@SvgIcon.Trash">Delete</GridCommandButton>
            <GridCommandButton Command="Cancel" Icon="@SvgIcon.Cancel" ShowInEdit="true">Cancel</GridCommandButton>
        </GridCommandColumn>
    </GridColumns>
</TelerikGrid>

@logger

@code {
    async Task AddHandler(GridCommandEventArgs args)
    {
        //Set default values for new items
        ((SampleData)args.Item).Name = "New Item Name";

        //Cancel if needed
        //args.IsCancelled = true;
    }

    async Task EditHandler(GridCommandEventArgs args)
    {
        SampleData item = (SampleData)args.Item;

        //prevent opening for edit based on condition
        if (item.ID < 3)
        {
            args.IsCancelled = true;//the general approach for cancelling an event
        }

        AppendToLog("Edit", args);
    }

    async Task UpdateHandler(GridCommandEventArgs args)
    {
        AppendToLog("Update", args);

        SampleData item = (SampleData)args.Item;

        // perform actual data source operations here through your service
        await MyService.Update(item);

        // update the local view-model data with the service data
        await GetGridData();
    }

    async Task DeleteHandler(GridCommandEventArgs args)
    {
        AppendToLog("Delete", args);

        SampleData item = (SampleData)args.Item;

        // perform actual data source operation here through your service
        await MyService.Delete(item);

        // update the local view-model data with the service data
        await GetGridData();

    }

    async Task CreateHandler(GridCommandEventArgs args)
    {
        AppendToLog("Create", args);

        SampleData item = (SampleData)args.Item;

        // perform actual data source operation here through your service
        await MyService.Create(item);

        // update the local view-model data with the service data
        await GetGridData();
    }

    async Task CancelHandler(GridCommandEventArgs args)
    {
        AppendToLog("Cancel", args);

        SampleData item = (SampleData)args.Item;

        // if necessary, perform actual data source operation here through your service

        await Task.Delay(1000); //simulate actual long running async operation
    }

    // this method and field just display what happened for visual cues in this example

    MarkupString logger;
    void AppendToLog(string commandName, GridCommandEventArgs args)
    {
        string currAction = string.Format(
            "<br />Command: <strong>{0}</strong>; is cancelled: <strong>{1}</strong>; is the item new: <strong>{2}</strong>",
                commandName,
                args.IsCancelled,
                args.IsNew
            );
        logger = new MarkupString(logger + currAction);
    }


    // in a real case, keep the models in dedicated locations, this is just an easy to copy and see example
    public class SampleData
    {
        public int ID { get; set; }
        [Required]
        public string Name { get; set; }
    }

    List<SampleData> MyData { get; set; }

    async Task GetGridData()
    {
        MyData = await MyService.Read();
    }

    protected override async Task OnInitializedAsync()
    {
        await GetGridData();
    }


    // the following static class mimics an actual data service that handles the actual data source
    // replace it with your actual service through the DI, this only mimics how the API will look like and works for this standalone page
    public static class MyService
    {
        private static List<SampleData> _data { get; set; } = new List<SampleData>();

        public static async Task Create(SampleData itemToInsert)
        {
            await Task.Delay(1000); // simulate actual long running async operation

            itemToInsert.ID = _data.Count + 1;
            _data.Insert(0, itemToInsert);
        }

        public static async Task<List<SampleData>> Read()
        {
            await Task.Delay(1000); // simulate actual long running async operation

            if (_data.Count < 1)
            {
                for (int i = 1; i < 50; i++)
                {
                    _data.Add(new SampleData()
                    {
                        ID = i,
                        Name = "Name " + i.ToString()
                    });
                }
            }

            return await Task.FromResult(_data);
        }

        public static async Task Update(SampleData itemToUpdate)
        {
            await Task.Delay(1000); // simulate actual long running async operation

            var index = _data.FindIndex(i => i.ID == itemToUpdate.ID);
            if (index != -1)
            {
                _data[index] = itemToUpdate;
            }
        }

        public static async Task Delete(SampleData itemToDelete)
        {
            await Task.Delay(1000); // simulate actual long running async operation

            _data.Remove(itemToDelete);
        }
    }
}

The grid events use EventCallback and can be synchronous or asynchronous. The example above shows async versions, and the signature for synchronous events is void <MethodName>(GridCommandEventArgs args).

Notes

There are a few considerations to keep in mind with the CUD operations of the grid. They are explained in the following list:

  • It is up to the data access logic to save the data once it is changed in the data collection. The example above showcases when that happens and adds some code to provide a visual indication of the change. In a real application, the code for handling data updates may be entirely different.

    • For example, you may want to update the view-model only on success of the data service with the model returned from the server. Another thing you may want to do is to inform the user for server (async, remote) validation errors such as duplicates. You can find examples of both in the Remote Validation sample project.
  • The CRUD event handlers must be async Task and not async void. A Task can be properly awaited and allows working with services and contexts, and lets the grid update after the actual data source operations complete.

    • When the method returns void, the execution of the context operations is not actually awaited, and you may get errors from the context (such as "Cannot access a disposed object. A common cause of this error is disposing a context that was resolved from dependency injection and then later trying to use the same context instance elsewhere in your application" or "A second operation started on this context before a previous operation completed. This is usually caused by different threads using the same instance of DbContext"). The grid may also re-render before the actual data update happens and you may not see the result.
  • If you are using the OnRead event to optimize the data requests, it will fire after the CUD events (OnCreate, OnUpdate, OnDelete, OnCancel) so that the grid data can be refreshed properly from the real data source. If you want to avoid such calls to the database, you can raise a flag in those four events to avoid calling your data service in the OnRead event, and then you can lower that flag at the end of OnRead so subsequent calls can fetch fresh data.

  • The Grid uses Activator.CreateInstance<TItem>(); to generate a new item when an Insert or Edit action is invoked, so the Model should have a parameterless constructor defined. If you cannot have such a constructor, you must use the OnModelInit event.

    • Another case when you may need to insert items through the grid state is when you use OnRead with grouping. In such cases the Grid is bound to an object, not to a particular model. As a result, it can't create new items for you and errors may be thrown. A workaround might be invoking Edit/Insert through the grid state and creating the object with your own code.
  • While editing, the Grid creates a copy of your original object which has a different reference. You receive that copy in the OnUpdate event handler. The OnEdit event receives the original item from the pristine Data collection, because it is a cancellable event and fires before the grid logic creates the copy. The built-in editors and editor templates receive the copy for their context that the grid will create after OnEdit.

    • For the Grid to successfully create a copy of the original object, all properties must have а setter and must not be readonly. Otherwise, editing may stop working.
  • If you want to pre-populate values to the user, see the Setting default values in new row KnowledgeBase article.

  • When you are using your Entity Framework models directly in the Grid (especially in a server-side Blazor scenario) and you use the Item property of GridCommandEventArgs directly in your DataBase update method, you can get one of the following exceptions: The instance of entity type 'YourModel' cannot be tracked because another instance with the same key value for {'Id'} is already being tracked. When attaching existing entities, ensure that only one entity instance with a given key value is attached... or This is a DynamicProxy2 error: The interceptor attempted to 'Proceed' for method 'Microsoft.EntityFrameworkCore.Infrastructure.ILazyLoader get_LazyLoader()' which has no target. When calling method without target there is no implementation to 'proceed' to and it is the responsibility of the interceptor to mimic the implementation (set return value, out arguments etc).

    To fix it you can change the update using this approach:

    1. Find the object in your database by Id (you can use dbContext.Find() or similar method depending on your infrastructure).
    2. Apply all the changes you need to it one by one - assign the values of all of its properties - dbObject.Property1 = argsItem.Property1...
    3. Call dbContext.SaveChanges()
  • The Grid validation is based on the DataAnnotationValidator and creates its own EditContext for a row that is in edit/insert mode. When the row is not in edit/insert mode, the EditContext is null. The EditContext is a cascading parameter and overrides any cascading parameters from parent components (such as an <EditForm> that may wrap the grid).

    • The validation will not be enabled for Grids bound to Expando objects or Dictionaries (such as DataTable).

    • When an input receives an EditContext (usually comes down as a cascading parameter), the framework also requires a ValueExpression. If you use two-way binding (the @bind-Value syntax), the ValueExpression is deducted from there. However, if you use only the Value property, you have to pass the ValueExpression yourself. This is a lambda expression that tells the framework what field in the model to update. The following sample demonstrates how to achieve that. You can also check the Requires a value for ValueExpression knowledge base article for more details.

    <EditorTemplate>
        <TelerikTextBox Value="@myModel.MyField"
                        ValueExpression="@( () => myModel.MyField )">
        </TelerikTextBox>
    </EditorTemplate>
    
    @* Applies to the other input type components as well *@
    
  • If you want to perform other data operations while the component is in Edit mode (applicable for InCell and Inline editing) the following behavior will occur:

    • For operations like Filter, Group, Sort, Page, Search, Select, Row drag and Delete:

      • InCell edit - if the validation is satisfied, a save operation will be executed. If the validation is not satisfied, editing will be cancelled, the OnCancel event will fire and the new user operation will be executed. In this case, you can handle the OnCancel event and set the IsCancelled property of the CommandEventArgs to true. Thus, the other data operation will be aborted and the Grid will remain in edit mode.
      • Inline edit - regardless of the validation, editing will be cancelled, the OnCancel event will fire and the new user operation will be executed. In this case, you can handle the OnCancel event and set the IsCancelled property of the CommandEventArgs to true. Thus, the other data operation will be aborted and the Grid will remain in edit mode.
    • For operations like Edit, Add, Save:

      • InCell edit - if the validation is satisfied, the currently edited item will be saved and the command will be executed. If the validation is not satisfied, the command will be blocked until the item is valid or editing is cancelled.
      • Inline edit - if the validation is satisfied, OnCancel will be fired for the currently edited item and the command will be executed. If the validation is not satisfied, the command will be blocked until the item is valid or editing is cancelled.
  • When editing a master row in a hierarchy Grid, the respective DetailTemplate will collapse unless you override the Equals() method of the master data item class.

See Also

In this article