Quantcast
Channel: Developer Express Inc.
Viewing all articles
Browse latest Browse all 2389

Connect a WinForms Data Grid to an Arbitrary ASP.NET Core WebAPI Service Powered by EF Core — Add Editing Features (Part 2)

$
0
0

I started a short series of posts a little while ago, titled Connect a WinForms Data Grid to an Arbitrary ASP.NET Core WebAPI Service Powered by EF Core. It relates to my previously published post Modern Desktop Apps And Their Complex Architectures and aims to illustrate an application system architecture that includes a data access service in addition to a WinForms application with a DevExpress Data Grid. The first post demonstrated the basic binding using the VirtualServerModeSource component, and this time I will explain how editing features can be activated in the Data Grid, interfacing with the same backend service.

We need to add to the list of basic assumptions I described in the first post. My backend service uses EF Core for data access — I should point out that this is not too relevant! It makes it easy to demonstrate for a blog post how data modifications can be implemented in such a service, and it is of course a common choice. But the architecture and the binding to the frontend would be precisely the same if the service used some entirely different data storage system, or even if it worked on a platform other than .NET.

Here’s the new assumption: the backend must allow for data to be modified through the accessible endpoints. There are various patterns for such data modifications, in this demo I chose to implement a REST-style access pattern that uses HTTP verbs to indicate the intent of a modification, and that accepts objects of the data transfer type through its endpoints (as opposed to some message or event based patterns, for instance).

Table of Contents

We also have related blog series, which may be of interest for you as well: JavaScript — Consume the DevExpress Backend Web API with Svelte (7 parts from data editing to validation, localization, reporting).

The demo repository

You can find the sample code for this demo in the GitHub repository. The Readme file describes how to run the sample. Please contact us if you have any questions or comments!

If you are interested in a few technical points about the sample code, please read on for a description of the sample structure and some of the relevant code files.

New POST, PUT and DELETE endpoints

Here are the three new endpoints:

app.MapPost("/data/OrderItem", async (DataServiceDbContext dbContext, OrderItem orderItem) =>
{
    dbContext.OrderItems.Add(orderItem);
    await dbContext.SaveChangesAsync();
    return Results.Created($"/data/OrderItem/{orderItem.Id}", orderItem);
});

app.MapPut("/data/OrderItem/{id}", async (DataServiceDbContext dbContext, int id, OrderItem orderItem) =>
{
    if (id != orderItem.Id)
    {
        return Results.BadRequest("Id mismatch");
    }

    dbContext.Entry(orderItem).State = EntityState.Modified;
    await dbContext.SaveChangesAsync();
    return Results.NoContent();
});

app.MapDelete("/data/OrderItem/{id}", async (DataServiceDbContext dbContext, int id) =>
{
    var orderItem = await dbContext.OrderItems.FindAsync(id);

    if (orderItem is null)
    {
        return Results.NotFound();
    }

    dbContext.OrderItems.Remove(orderItem);
    await dbContext.SaveChangesAsync();
    return Results.NoContent();
});

These implementations are quite standard in the way they handle parameters and return values. The ASP.NET Core methods MapPost, MapPut, MapDelete, in addition to the MapGet that was already used in the previous version of the sample, establish the handling of different HTTP verbs with the requests. The runtime infrastructure of ASP.NET Core handles details automatically, such as the OrderItem parameter received in the MapPut handler, which is deserialized from its JSON representation using the existing EF Core type.

The editing form

The sample source code includes a simple editing form which will be used for both editing of existing rows and creating new rows. There are no surprises in this implementation, a couple of static methods provide an interface to call from the main form of the sample application.

A new abstraction: the class DataServiceClient

In my first post, I kept things simple and encoded the call to the data service, using an HttpClient instance, within the VirtualServerModeDataLoader class. A few lines of code handled the instantiation of the HttpClient with the correct base URL, and the process of fetching a specific URL and dealing with the JSON results. Since it now became clear that additional interface calls to the data service will be required, it made sense to abstract this logic and I created the new type DataServiceClient.

The existing code in VirtualServerModeDataLoader now simply calls a method on the DataServiceClient:

var dataFetchResult = await DataServiceClient.GetOrderItemsAsync(
  e.CurrentRowCount, BatchSize, SortField, SortAscending);

Other methods exist in this class to handle the various use cases. Updates use the simplest algorithm, by encoding the transfer object as JSON and sending it to the service URL using a PUT request.

public static async Task UpdateOrderItemAsync(OrderItem orderItem)
{
  using var client = CreateClient();
  var response = await client.PutAsync($"{baseUrl}/data/OrderItem/{orderItem.Id}",
    new StringContent(JsonSerializer.Serialize(orderItem), 
      Encoding.UTF8, "application/json"));
  response.EnsureSuccessStatusCode();
}

The process of creating a new item is a little bit more complicated. In this case, the service returns the object, which can be important if you allow values to be generated or modified on the server side during creation. In the demo setup, this applies to the primary key on the OrderItem EF type, which is an auto-generated int value. It is of course up to a client application to take advantage of this information where possible, but since the DataServiceClient is meant to be an example of a general purpose implementation it seems good practice to implement it fully. Note that this pattern introduces some overhead since it requires data to flow in both directions. It’s a good general recommendation to allow the client to generate key values by using a Guid type. But server-generated values are still common, so the implementation takes this into account.

static OrderItem? AsOrderItem(this string responseBody)
{
  return JsonSerializer.Deserialize<OrderItem>(responseBody, 
    new JsonSerializerOptions
    {
      PropertyNameCaseInsensitive = true
    });
}

public static async Task<OrderItem?> CreateOrderItemAsync(OrderItem orderItem)
{
  using var client = CreateClient();
  var response = await client.PostAsync($"{baseUrl}/data/OrderItem",
    new StringContent(JsonSerializer.Serialize(orderItem), 
      Encoding.UTF8, "application/json"));
  response.EnsureSuccessStatusCode();
  var responseBody = await response.Content.ReadAsStringAsync();
  return responseBody.AsOrderItem();
}

Finally, for deletion there is more special handling in place. While both of the methods illustrated above detect any errors which occurred on the server (using the EnsureSuccessStatusCode helper), they don’t make any effort to handle such errors. In reality you may want to do a bit more work here, at least to log the errors — in the demo project I left this out for brevity. You could also choose to handle these cases like deletion, as I’m explaining now.

On the UI level we have a chance to cancel an operation the user began (in this case deletion, but the idea also applies to creation or modification of data rows) if we find that it cannot complete as expected due to an error returned by the data service. You will see in a moment what the UI level code looks like, but in the DataServiceClient I implemented deletion a bit differently, by catching any errors and return a simple boolean status instead.

public static async Task<bool> DeleteOrderItemAsync(int id)
{
  try
  {
    using var client = CreateClient();
    var response = await client.DeleteAsync($"{baseUrl}/data/OrderItem/{id}");
    response.EnsureSuccessStatusCode();
    return true;
  }
  catch (Exception ex)
  {
    Debug.WriteLine(ex);
    return false;
  }
}

The sensible action to log any errors for future analysis is indicated only by the Debug.WriteLine call in this code. The return value however allows the caller to find out whether the deletion was executed successfully, and react accordingly.

The interface with the UI

While the process of data loading, as I described in the previous post, used a specific built-in mechanism in the shape of the VirtualServerModeSource, there is no similar standard feature for editing operations. Instead, we need to use the API of the Data Grid to handle click events for editing, and to retrieve selected elements as needed.

To edit a row, I added a simple double-click event handler to the GridView.

private async void gridView1_DoubleClick(
  object sender, EventArgs e)
{
  if (sender is GridView view)
  {
    if (view.FocusedRowObject is OrderItem oi)
    {
      var editResult = EditForm.EditItem(oi);
      if (editResult.changesSaved)
      {
        await DataServiceClient.UpdateOrderItemAsync(editResult.item);
        view.RefreshData();
      }
    }
  }
}

Using the property FocusedRowObject on the view, we retrieve the selected row by its object type. This object is then passed to the API in the EditForm, which returns a flag indicating whether the user confirmed any edits by clicking Save. In this case, the DataServiceClient comes in to persist the changed object back to the data service. After a refresh of the view data, the process is complete.

To enable adding of rows, I added a toolbar button to the form. Its click handler has this code, which is similar to the update logic.

private async void addItemButton_ItemClick(
  object sender, DevExpress.XtraBars.ItemClickEventArgs e)
{
  if (gridControl.FocusedView is ColumnView view)
  {
    var createResult = EditForm.CreateItem();
    if (createResult.changesSaved)
    {
      await DataServiceClient.CreateOrderItemAsync(createResult.item!);
      view.RefreshData();
    }
  }
}

A final toolbar button allows users to delete the focused object. Again, we simply call into the DataServiceClient functionality to make this happen.

private async void deleteItemButton_ItemClick(
  object sender, DevExpress.XtraBars.ItemClickEventArgs e)
{
  if (gridControl.FocusedView is ColumnView view &&
      view.GetFocusedRow() is OrderItem orderItem)
  {
    await DataServiceClient.DeleteOrderItemAsync(orderItem.Id);
    view.RefreshData();
  }
}

That is all for this step. As you saw, both the implementation of the editing features in the service and the binding to the UI are easy to do. As ever, there may be some extra details to observe in a real-world solution, but the few lines of code this demo has so far brought us pretty far already!

Future plans for this post series

As I announced previously, the next post will cover authentication and authorization mechanisms that become necessary, but also possible, in the service-based application architecture I’m describing. In addition, I’m now planning a second upcoming post that describes how to activate various other Data Grid features, like Total Summaries and Filtering.

Please send your feedback — thanks to all of you who have already done that! — and any questions or ideas, we will attempt to consider everything!


Viewing all articles
Browse latest Browse all 2389

Trending Articles