Radzen datagrid and overridden equals/hashcode

I have a radzen Datagrid defined. nothing too fancy or complex, just columns and basic edit template for each.

<RadzenDataGrid @ref=@bulkPriceGrid Data="@BulkPrice" TItem="BulkPrice">
    <Columns>
        <RadzenDataGridColumn TItem="BulkPrice" Property="StartingCount" Title="Starting Count" SortOrder="SortOrder.Ascending">
            <EditTemplate Context="price">
                <RadzenNumeric @bind-Value=@price.StartingCount Step="100" />
            </EditTemplate>
        </RadzenDataGridColumn>
        <RadzenDataGridColumn TItem="BulkPrice" Property="EndingCount" Title="Ending Count">
            <EditTemplate Context="price">
                <RadzenNumeric @bind-Value=@price.EndingCount Step="100" />
            </EditTemplate>
        </RadzenDataGridColumn>
        <RadzenDataGridColumn TItem="BulkPrice" Property="Price" Title="Cost">
            <EditTemplate Context="price">
                <RadzenNumeric @bind-Value=@price.Price />
            </EditTemplate>
        </RadzenDataGridColumn>
        <AuthorizeView Roles="BOM.Write" Context="AuthBomWrite">
            <RadzenDataGridColumn TItem="BulkPrice" Context="price" Frozen="true" FrozenPosition="FrozenColumnPosition.Right">
            <Template Context="price">
                <RadzenButton Icon="edit" ButtonStyle="ButtonStyle.Light" Variant="Variant.Flat" Size="ButtonSize.Medium" Click="@(args => EditRow(price))" @onclick:stopPropagation="true" />
                <RadzenButton ButtonStyle="ButtonStyle.Danger" Icon="delete" Variant="Variant.Flat" Shade="Shade.Lighter" Size="ButtonSize.Medium" class="my-1 ms-1" Click="@(args => DeleteRow(price))" @onclick:stopPropagation="true" />
            </Template>
            <EditTemplate Context="price">
                <RadzenButton Icon="check" ButtonStyle="ButtonStyle.Success" Variant="Variant.Flat" Size="ButtonSize.Medium" Click="@((args) => SaveRow(price))" />
                <RadzenButton Icon="close" ButtonStyle="ButtonStyle.Light" Variant="Variant.Flat" Size="ButtonSize.Medium" class="my-1 ms-1" Click="@((args) => CancelEdit(price))" />
                <RadzenButton ButtonStyle="ButtonStyle.Danger" Icon="delete" Variant="Variant.Flat" Shade="Shade.Lighter" Size="ButtonSize.Medium" class="my-1 ms-1" Click="@(args => DeleteRow(price))" />
            </EditTemplate>
        </RadzenDataGridColumn>
        </AuthorizeView>
    </Columns>
</RadzenDataGrid>
    <RadzenButton ButtonStyle="ButtonStyle.Success" Icon="add_circle_outline" Text="Add New Bulk Threshold" Style="margin-top: 10px"
        Disabled=@(priceToInsert != null || priceToUpdate != null) Click="AddNewPrice" />

here is the backing code

@code {
    [Parameter]
    public string Title { get; set; }

    [Parameter]
    public List<BulkPrice> BulkPrice { get; set; }

    [Parameter]
    public string Style { get; set; }

    private RadzenDataGrid<BulkPrice>? bulkPriceGrid;
    private BulkPrice? priceToInsert, priceToUpdate = null;

    private void Reset()
    {
        priceToInsert = null;
        priceToUpdate = null;
    }

    private async Task AddNewPrice()
    {
        priceToInsert = new BulkPrice();
        priceToInsert.Changes = EChangeable.New;
        await bulkPriceGrid.InsertRow(priceToInsert);
    }

    private async Task EditRow(BulkPrice price)
    {
        priceToUpdate = new BulkPrice(price);
        await bulkPriceGrid.EditRow(price);

        StateHasChanged();
    }

    private void DeleteRow(BulkPrice price)
    {
        Reset();
        BulkPrice.Remove(price);
    }

    private async Task SaveRow(BulkPrice price)
    {
        Reset();
        
        await bulkPriceGrid.UpdateRow(price);
        
        BulkPrice.Add(price);
        await bulkPriceGrid.RefreshDataAsync();
        StateHasChanged();
    }

    private async Task CancelEdit(BulkPrice price)
    {
        var tempPrice = priceToUpdate;
        Reset();

        bulkPriceGrid.CancelEditRow(price);

        var priceIndex = BulkPrice.IndexOf(price);
        if (priceIndex < 0)
            return;

        BulkPrice.Remove(price);
        BulkPrice.Insert(priceIndex, tempPrice);

        await bulkPriceGrid.RefreshDataAsync();
    }

    private void FieldChanged(Changeable obj)
    {
        obj.Changes = EChangeable.Updated;
    }

    private void CellRender(CellRenderEventArgs<BulkPrice> args)
    {
        args.Attributes.Add("class", $"changeable-{args.Data.Changes.ToString()}");
    }
}

the model being represented is simple as well

public sealed class BulkPrice : Changeable
{
    public Guid BulkPriceId { get; set; } = Guid.NewGuid();
    public int StartingCount { get; set; }
    public int EndingCount { get; set; }
    public decimal Price { get; set; }

    public BulkPrice() { }
    public BulkPrice(BulkPrice newPrice)
    {
        StartingCount = newPrice.StartingCount;
        EndingCount = newPrice.EndingCount;
        Price = newPrice.Price;
        Changes = newPrice.Changes;
    }

    public override bool Equals(object? obj)
    {
        if(obj == null) return false;
        if(ReferenceEquals(obj, this)) return true;
        if(obj.GetType() != GetType()) return false;

        BulkPrice other = obj as BulkPrice;

        return StartingCount == other.StartingCount
            && EndingCount == other.EndingCount
            && Price == other.Price
            && Changes == other.Changes
            && BulkPriceId == other.BulkPriceId;

    }

    public override int GetHashCode()
    {
        return HashCode.Combine(BulkPriceId, StartingCount, EndingCount, Price, Changes);
    }
public enum EChangeable
{
    None,
    New,
    Updated,
    Removed
}

public abstract class Changeable
{
    private EChangeable _changes = EChangeable.None;
    public EChangeable Changes { get => _changes;
        set
        {
            if (value == EChangeable.None) { _changes = EChangeable.None; }
            else if (value == EChangeable.Removed) { _changes = value; }
            else if (value == EChangeable.New) { _changes = value; }
            else if (value == EChangeable.Updated && _changes == EChangeable.None) { _changes = value; }
            
        }
    }
}
}

what makes this bulk price class unique is that the equals and gethashcode methods have been overridden. this shouldn't be a problem but what I'm encountering is that when I go to add a new row to the datagrid, the defaults work fine and I can click the checkbox that saves the object (row/record). but if I make any changes to the values before i click the checkbox, i get an error in the SaveRow method on instruction await bulkPriceGrid.UpdateRow(price). the error being

System.Collections.Generic.KeyNotFoundException: The given key 'MyNameSpace.BulkPrice' was not present in the dictionary.
   at System.Collections.Generic.Dictionary`2.get_Item(TKey key)
   at Radzen.Blazor.RadzenDataGrid`1.UpdateRow(TItem item)
   at MyNameSpace.BulkPriceComponent.SaveRow(BulkPrice price)

This error points to some the datagrid not using the same hash value as what my object generates. If I comment out the equals and gethashcode methods, it works fine.

I'm afraid that I cannot run the code you've posted - there are missing parts. You can use our Order class used in our InLine edit example to simulate the issue at your end:


Simply overriding Equals and GetHashCode() will not cause any problems, let us know what we need to write in these methods to simulate your case.

dg-inline

the GetHashCode method on the order class should do more than just base.GetHashCode(). It should look something like this:

public override int GetHashCode()
{
    return HashCode.Combine(OrderID, Customer.CustomerID, EmployeeID);
}

I've pushed fix that should enable such cases - it will be part of our next update before the end of the week: