In the post Modern Desktop Apps And Their Complex Architectures I summarized a while ago how the world of application architecture has evolved in the last years. Our WinForms team published a follow-up with WinForms — Connect a .NET Desktop Client to Secure Backend Web API Service (EF Core with OData). In this post I will take another step back and demonstrate how to use an even simpler service setup together with the DevExpress WinForms Data Grid.
Basic assumptions
Many application systems which originated as desktop apps have been extended over time with data access services that work independently of any original client-side direct binding patterns. For instance, web applications or mobile frontends may have entered the picture at some point, and this necessitated a broader look at the data access architecture. On the other hand, perhaps your application system has not gone through such steps yet!
The idea is, in either case, to shift the direct connection to a database server, e.g. using port 1433 for a Microsoft SQL Server, to a place where it’s not the responsibility of the desktop app anymore. This may be a requirement for maintenance reasons, once your system has more than one client, or it may be done to facilitate a cleaner architecture.
For purposes of this demo, the data service will be quite simple. We assume that it uses Entity Framework Core for data access, but the point is only that data access should not be difficult on the level of the service. Equally we assume that it’s easy to add features to the service as needed — of course that’s not always true, but in this post we will not yet focus on the complicated scenarios where the service cannot be touched.
The final assumption we make is that the service is more “general purpose” than the one described in the OData-related post linked above. This is not meant to say that using OData is a bad idea, but for this demo we’ll do without it.
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.
The Backend: an ASP.NET Core WebAPI service using Entity Framework Core
The backend project is called DataService
and it
was created using the standard ASP.NET Core WebAPI template. It
uses top-level statements and the “minimal API” configuration
format for the service, it does not include anything that’s not
necessary for this demo — all so you can focus on the code
required for the demo setup itself. There are two endpoint
handlers in the service, one to generate some test data and the
other to query data.
This second handler, at the URL /data/OrderItems
,
is the important one for this post. For the sample
implementation, the handler accepts several optional parameters
to support the skip, take and
sort features. The code is simple, it queries data
from the Entity Framework Core database context, and uses the
standard IQueryable<T>
based helpers to
implement the data shaping functionality. The
TotalCount
field is returned together with the
data since we need this on the client side to determine how
much data is available to query.
app.MapGet("/data/OrderItems", async (
DataServiceDbContext dbContext,
int skip = 0, int take = 20,
string sortField = "Id", bool sortAscending = true) =>
{
var source =
dbContext.OrderItems.AsQueryable()
.OrderBy(sortField + (sortAscending ? " ascending" : " descending"));
var items = await source.Skip(skip).Take(take).ToListAsync();
var totalCount = await dbContext.OrderItems.CountAsync();
return Results.Ok(new
{
Items = items,
TotalCount = totalCount
});
});
To be a bit more abstract about it: this endpoint handler exemplifies the service functions that you’ll need in a data service, in order to provide required information to a frontend application, or a specialized component like the Data Grid. The implementation and the set of supported data shaping features will vary, but logically any data access will require some endpoints which work along these lines.
The Frontend: a Windows Forms app with a DevExpress Data Grid
In the project WinForms.Client
you’ll find the
class OrderItem
. This is the type the client works
with, representing the data on the backend. However, note that
this type is not the same as the one the backend uses! If you
compare it carefully to the OrderItem
in the
DataService
, you’ll find that the frontend type
does not exhibit the same Entity Framework Core artifacts as
the backend type. Specifically the virtual
keyword
is absent from the property declarations in the frontend type.
In a real application these types may (and probably would!) differ more. The sample setup is simple and the data types were introduced for demonstration purposes only, but in reality the discrepancies between a backend persistent type and a frontend model of service-retrieved data will likely be much more obvious.
In the MainForm
of the Windows Forms application,
the GridControl
component is configured with
columns corresponding to the properties of the
OrderItem
. The component is bound to a
VirtualServerModeSource instance on the form,
whose RowType
property was set to
OrderItem
. This allows the grid to discover the
columns from the data source automatically.
To fetch the data, the
VirtualServerModeSource
uses at least two event
handlers (though one of them is optional depending on
circumstances). Code for the handlers of the
ConfigurationChanged
and
MoreRows
events can be found in
MainForm.cs
.
The ConfigurationChanged
handler is executed when
the grid changes some relevant part of its runtime
configuration as a reaction to user interaction, e.g. when the
user clicks a column header to apply sorting. The
MoreRows
handler comes in when an initial fetch
operation returns a result which indicates that more data is
available. In this case, the grid attempts to retrieve more
rows if and when the user scrolls to the bottom of the
currently loaded set of data.
In future posts we will go into some more depth about the
VirtualServerModeSource
, but of course there is
also
complete documentation
for this component if you want to know more.
In the sample, loading logic for the virtual data source is
encapsulated in the class
VirtualServerModeDataLoader
, which is instantiated
by the ConfigurationChanged
event handler, i.e. on
each change of the grid’s runtime configuration by the end
user. The loader class receives the current configuration on
instantiation, and the code shows, as an example, how to
extract sorting details and remember them for later
application.
public VirtualServerModeDataLoader(
VirtualServerModeConfigurationInfo configurationInfo)
{
// For instance, let's assume the backend supports sorting for just one field
if (configurationInfo.SortInfo?.Length > 0)
{
SortField = configurationInfo.SortInfo[0].SortPropertyName;
SortAscending = !configurationInfo.SortInfo[0].IsDesc;
}
}
public string SortField { get; set; } = "Id";
public bool SortAscending { get; set; } = true;
Data loaded from the backend is encoded as JSON in the sample,
although different encodings like gRPC/Protocol Buffers would
work equally well. The type DataFetchResult
models
the structure that is published by the backend endpoint,
including the TotalCount
information field.
public class DataFetchResult
{
public List<OrderItem> Items { get; set; } = null!;
public int TotalCount { get; set; }
}
Finally, the method GetRowsAsync
handles the
actual retrieval of data. It is called both on initial load
(from the ConfigurationChanged
handler) and on
further loads (from the MoreRows
handler), but the
difference is only that the CurrentRowCount
field
of the event args differs between these applications.
The HttpClient
is used to retrieve the data,
passing the arguments as URL parameters for skip
,
take
and the sorting properties. The results are
deserialized from JSON and returned together with the
moreRowsAvailable
flag, which is interpreted by
the grid as previously described.
public Task<VirtualServerModeRowsTaskResult>
GetRowsAsync(VirtualServerModeRowsEventArgs e)
{
return Task.Run(async () =>
{
using var client = new HttpClient();
var response = await client.GetAsync(
$"{System.Configuration.ConfigurationManager.AppSettings["baseUrl"]}/data/OrderItems?skip={e.CurrentRowCount}&take={BatchSize}&sortField={SortField}&sortAscending={SortAscending}");
response.EnsureSuccessStatusCode();
var responseBody = await response.Content.ReadAsStringAsync();
var dataFetchResult =
JsonSerializer.Deserialize<DataFetchResult>(
responseBody, new JsonSerializerOptions
{
PropertyNameCaseInsensitive = true
});
if (dataFetchResult is null)
return new VirtualServerModeRowsTaskResult();
var moreRowsAvailable =
e.CurrentRowCount + dataFetchResult.Items.Count < dataFetchResult.TotalCount;
return new VirtualServerModeRowsTaskResult(
dataFetchResult.Items, moreRowsAvailable);
}, e.CancellationToken);
}
This completes the first implementation of a Data Grid binding to an independent service. I recommend you clone the repository and try the sample for yourself!
Outlook and questions
As I mentioned before, there are two parts we will certainly add to this sample:
- Editing in the Data Grid, and correspondingly write access to the data through the service
- Authentication and authorization support.
Beyond that we are open to your suggestions for the direction this may take. Please let us know which questions are foremost in your mind after reading this and trying the sample!