Example T1258223
Visible to All Users

Connect a WinForms Data Grid to an ASP.NET Core WebAPI Service Powered by EF Core — Authenticate Users and Protect Data

NOTE: This example extends the capabilities (introduces user login and permission-based access control) in the following example: Connect the DevExpress WinForms Data Grid to a .NET Core Service and Enable Data Editing.

Refer to the following step-by-step tutorial to run the example: Getting Started.

This example adds a key security feature to the application: user login and permission-based access control (uses Resource Owner Password Credentials (ROPC) for authentication).

Run and Configure Keycloak

Follow the steps below to set up and configure Keycloak, an open-source identity and access management framework. Our example uses Docker to run Keycloak locally and configure roles/users:

  1. Install Docker.
  2. Execute the following command to run Keycloak in a Docker Container:
    Code
    > docker run -p 8080:8080 -e KEYCLOAK_ADMIN=admin -e KEYCLOAK_ADMIN_PASSWORD=admin -v ./data:/opt/keycloak/data quay.io/keycloak/keycloak:latest start-dev
    • -e KEYCLOAK_ADMIN, -e KEYCLOAK_ADMIN_PASSWORD -- set the initial admin credentials.
    • -v ./data:/opt/keycloak/data -- mounts the container's data directory to your host for persistence purposes.
    • -p 8080:8080 -- exposes Keycloak on port 8080 of your host machine.
  3. Access Keycloak Admin Console
    • Open your browser and go to http://localhost:8080/admin.
    • Log in using the admin credentials set in the previous step (admin/admin).
  4. Create a New Realm
    • In the left navigation menu, navigate to Clients and click Create Client.
    • Set a Client ID (for example, app1) and click Next.
    • Ensure Direct access grants is enabled, then click Save.
  5. Create a Role
    • Navigate to Realm Roles in the left menu and click Create role.
    • Name the role (e.g., writers) and click Save.
  6. Adjust Login Settings
    • Go to Realm Settings | Login tab.
    • Disable Login with email.
    • In the User Profile tab, disable the Required field for email.
  7. Create a User
    • Go to Users and click Create a new user.
    • Set Username (for example, writer), random first/last names, and save the account.
    • In the Credentials tab, set a password and deselect Temporary.
  8. Assign Role to the User
    Go to Role mapping, filter by realm roles, and assign the writers role.
  9. Create a Reader User
    Repeat steps 7 and 8 for a second user (for example, reader), but do not assign a role.

Activate Authentication and Authorization on the Server

  1. Configure JWT Authentication
    • In your DataService app, extend service initialization to use JWT Bearer authentication.
    • Add the following code to configure TokenValidationParameters in the Startup or Program class. This ensures that incoming requests are validated using JWT tokens issued by your Keycloak server:
      C#
      options.TokenValidationParameters = new TokenValidationParameters { ValidateIssuer = true, ValidIssuer = $"{builder.Configuration["Jwt:KeycloakUrl"]}/realms/{builder.Configuration["Jwt:Realm"]}", ValidateAudience = true, ValidAudience = builder.Configuration["Jwt:Audience"], ValidateLifetime = true, ValidateIssuerSigningKey = true, IssuerSigningKey = publicKey };
  2. Ensure your appsettings.json contains correct JWT settings. Adjust values to match your Keycloak configuration if necessary.
    JSON
    "Jwt": { "Issuer": "http://localhost:8080/realms/winappdemo", "Audience": "account", "KeycloakUrl": "http://localhost:8080", "Realm": "winappdemo" }
  3. Add Authorization Policies
    • Define a policy for the role writers in the Startup or Program class:
      C#
      builder.Services.AddAuthorization(o => { o.AddPolicy("writers", p => p.RequireRealmRole("writers")); });
    • Use the RequireRealmRole method to validate the role:
      C#
      public static class PolicyHelpers { public static void RequireRealmRole(this AuthorizationPolicyBuilder policy, string roleName) { policy.RequireAssertion(context => { var realmAccess = context.User.FindFirst("realm_access")?.Value; if (realmAccess == null) return false; var node = JsonNode.Parse(realmAccess); if (node == null || node["roles"] == null) return false; var array = node["roles"]!.AsArray(); return array.Select(r => r?.GetValue<string>()).Contains(roleName); }); } }
  4. Protect API Endpoints
    Use the RequireAuthorization method to secure API endpoints:
    • For open endpoints (e.g., /api/populateTestData), no authorization is required.
    • GET endpoints /data/OrderItems and /data/OrderItem/{id} call RequireAuthorization(), so that an authenticated user is required to successfully execute them, but no specific roles are needed.
    • The remaining endpoints POST to /data/OrderItem, and PUT and DELETE to /data/OrderItem/{id}, call RequireAuthorization("writers"), so that policy writers is applied and realm role writers is required.
      The example of the POST endpoint:
    C#
    app.MapPost("/data/OrderItem", async (DataServiceDbContext dbContext, OrderItem orderItem) => { dbContext.OrderItems.Add(orderItem); await dbContext.SaveChangesAsync(); return Results.Created($"/data/OrderItem/{orderItem.Id}", orderItem); }).RequireAuthorization("writers");

Enable User Logins in the WinForms App

The LoginForm prompts users for username and password. The form contains two DevExpress TextEdit controls for username and password fields.

LogIn Form

When a user clicks the "Log In" button, the LogIn form collects user credentials and sends a POST request to the Keycloak server to retrieve an access token:

C#
// LoginForm.cs private async void loginButton_Click(object sender, EventArgs e) { //... if (await DataServiceClient.LogIn(userNameEdit.Text, passwordEdit.Text)) { this.DialogResult = DialogResult.OK; this.Close(); } else { XtraMessageBox.Show("Username or password are invalid, or a technical error occurred.", "Error", MessageBoxButtons.OK, MessageBoxIcon.Error); } } // DataServiceClient.cs public static async Task<bool> LogIn(string username, string password) { //... var content = new FormUrlEncodedContent(new Dictionary<string, string> { {"client_id", clientId}, {"username", username}, {"password", password}, {"grant_type", "password"} }); var url = $"{authUrl}/realms/{realm}/protocol/openid-connect/token"; var response = await bareHttpClient.PostAsync(url, content); try { response.EnsureSuccessStatusCode(); var responseString = await response.Content.ReadAsStringAsync(); (accessToken, refreshToken, expiresIn) = GetTokens(responseString); if (accessToken != null) { lastRefreshed = DateTime.Now; (name, realmRoles) = GetUserDetails(accessToken); } return true; } catch (Exception ex) { Debug.WriteLine(ex); return false; } }

The GetTokens method parses the JSON response from Keycloak and extracts access_token, refresh_token, and expires_in fields:

C#
static (string? access_token, string? refresh_token, int? expires_in) GetTokens(string jsonString) { var node = JsonNode.Parse(jsonString); if (node == null) return (null, null, null); else return (node["access_token"]?.GetValue<string>(), node["refresh_token"]?.GetValue<string>(), node["expires_in"]?.GetValue<int>()); }

A client must check the validity and expiration of an access token before its use and use the refreshToken to retrieve a new access token if the old one has expired (see the BearerTokenHandler class). The Authorization request header is configured to pass the value of the current access token to the server using a specific format:

C#
protected override async Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken) { if (!String.IsNullOrWhiteSpace(accessToken)) { //... request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", accessToken); } return await base.SendAsync(request, cancellationToken); }

The client application can now authenticate against the Keycloak server and send an access token to the data service (confirming permission to access data endpoints). The server determines the roles associated with a specific logged-in user account and allows/denies access to endpoints accordingly.

Configure UI Based on Access Permissions

Decode the Access Token

Once the user logs in and the accessToken is retrieved, decode the token on the client side to access the user's roles. Add the System.IdentityModel.Tokens.Jwt NuGet package to your project to handle JWT decoding.

The GetUserDetails method extracts user details such as username and roles:

C#
static (string? name, string?[] realmRoles) GetUserDetails(string? accessToken) { if (String.IsNullOrEmpty(accessToken)) return (null, []); var handler = new JwtSecurityTokenHandler(); var token = handler.ReadJwtToken(accessToken); var claim = (string claimType) => token.Claims.FirstOrDefault(c => c.Type == claimType)?.Value; var name = claim("name"); var realmAccess = claim("realm_access"); var node = JsonNode.Parse(realmAccess); if (node == null || node["roles"] == null) return (name, []); var array = node["roles"]!.AsArray(); var realmRoles = array.Select(r => r?.GetValue<string>()).ToArray(); return (name, realmRoles); }

Evaluate User Roles

Evaluate user roles to determine which UI elements to enable/disable. The EvaluateRoles method enables/disables UI elements based on roles available to the user:

C#
// MainForms.cs private void EvaluateRoles() { if (DataServiceClient.LoggedIn) { if (DataServiceClient.UserHasRole("writers")) { userIsWriter = true; addItemButton.Enabled = true; deleteItemButton.Enabled = true; } else { userIsWriter = false; addItemButton.Enabled = false; deleteItemButton.Enabled = false; } } else { userIsWriter = false; addItemButton.Enabled = false; deleteItemButton.Enabled = false; } }

Evaluate User Roles

Files to Review

See Also

Does this example address your development requirements/objectives?

(you will be redirected to DevExpress.com to submit your response)

Example Code

WinForms.Client/DataServiceClient.cs
C#
using System.Diagnostics; using System.IdentityModel.Tokens.Jwt; using System.Net.Http; using System.Net.Http.Headers; using System.Text; using System.Text.Json; using System.Text.Json.Nodes; namespace WinForms.Client { public static class DataServiceClient { public class BearerTokenHandler : DelegatingHandler { public BearerTokenHandler() { InnerHandler = new HttpClientHandler(); } protected override async Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken) { if (!String.IsNullOrWhiteSpace(accessToken)) { if (!String.IsNullOrWhiteSpace(refreshToken) && lastRefreshed.HasValue && expiresIn.HasValue && DateTime.Now - lastRefreshed > TimeSpan.FromSeconds((int)(expiresIn - 60))) { if (string.IsNullOrEmpty(authUrl)) throw new InvalidOperationException("The 'authUrl' configuration setting is missing."); if (string.IsNullOrEmpty(realm)) throw new InvalidOperationException("The 'realm' configuration setting is missing."); if (string.IsNullOrEmpty(clientId)) throw new InvalidOperationException("The 'clientId' configuration setting is missing."); var content = new FormUrlEncodedContent(new Dictionary<string, string> { {"grant_type", "refresh_token"}, {"client_id", clientId}, {"refresh_token", refreshToken} }); var url = $"{authUrl}/realms/{realm}/protocol/openid-connect/token"; var response = await bareHttpClient.PostAsync(url, content); response.EnsureSuccessStatusCode(); var responseString = await response.Content.ReadAsStringAsync(); (accessToken, refreshToken, expiresIn) = GetTokens(responseString); if (accessToken != null) { lastRefreshed = DateTime.Now; (name, realmRoles) = GetUserDetails(accessToken); } } request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", accessToken); } return await base.SendAsync(request, cancellationToken); } } static DataServiceClient() { if (string.IsNullOrEmpty(baseUrl)) throw new InvalidOperationException("The 'baseUrl' configuration setting is missing."); } static string? baseUrl = System.Configuration.ConfigurationManager.AppSettings["baseUrl"]; static string? accessToken; public static bool LoggedIn => accessToken != null; static string? refreshToken; static int? expiresIn; static DateTime? lastRefreshed; static string? name; public static string? Name => name; static string?[]? realmRoles; public static bool UserHasRole(string role) => realmRoles != null && realmRoles.Contains(role); static string? clientId = System.Configuration.ConfigurationManager.AppSettings["clientId"]; static string? realm = System.Configuration.ConfigurationManager.AppSettings["realm"]; static string? authUrl = System.Configuration.ConfigurationManager.AppSettings["authUrl"]; static HttpClient bareHttpClient = new HttpClient(); static (string? access_token, string? refresh_token, int? expires_in) GetTokens(string jsonString) { var node = JsonNode.Parse(jsonString); if (node == null) return (null, null, null); else return (node["access_token"]?.GetValue<string>(), node["refresh_token"]?.GetValue<string>(), node["expires_in"]?.GetValue<int>()); } static (string? name, string?[] realmRoles) GetUserDetails(string? accessToken) { if (String.IsNullOrEmpty(accessToken)) return (null, []); var handler = new JwtSecurityTokenHandler(); var token = handler.ReadJwtToken(accessToken); var claim = (string claimType) => token.Claims.FirstOrDefault(c => c.Type == claimType)?.Value; var name = claim("name"); var realmAccess = claim("realm_access"); var node = JsonNode.Parse(realmAccess); if (node == null || node["roles"] == null) return (name, []); var array = node["roles"]!.AsArray(); var realmRoles = array.Select(r => r?.GetValue<string>()).ToArray(); return (name, realmRoles); } public static async Task<bool> LogIn(string username, string password) { if (string.IsNullOrEmpty(authUrl)) throw new InvalidOperationException("The 'authUrl' configuration setting is missing."); if (string.IsNullOrEmpty(realm)) throw new InvalidOperationException("The 'realm' configuration setting is missing."); if (string.IsNullOrEmpty(clientId)) throw new InvalidOperationException("The 'clientId' configuration setting is missing."); var content = new FormUrlEncodedContent(new Dictionary<string, string> { {"client_id", clientId}, {"username", username}, {"password", password}, {"grant_type", "password"} }); var url = $"{authUrl}/realms/{realm}/protocol/openid-connect/token"; var response = await bareHttpClient.PostAsync(url, content); try { response.EnsureSuccessStatusCode(); var responseString = await response.Content.ReadAsStringAsync(); (accessToken, refreshToken, expiresIn) = GetTokens(responseString); if (accessToken != null) { lastRefreshed = DateTime.Now; (name, realmRoles) = GetUserDetails(accessToken); } return true; } catch (Exception ex) { Debug.WriteLine(ex); return false; } } public static void LogOut() { accessToken = null; refreshToken = null; expiresIn = null; lastRefreshed = null; name = null; realmRoles = null; } static HttpClient authorizedHttpClient = new HttpClient(new BearerTokenHandler()); public static async Task<DataFetchResult?> GetOrderItemsAsync(int skip, int take, string sortField, bool sortAscending) { var response = await authorizedHttpClient.GetAsync( $"{baseUrl}/data/OrderItems?skip={skip}&take={take}&sortField={sortField}&sortAscending={sortAscending}"); response.EnsureSuccessStatusCode(); var responseBody = await response.Content.ReadAsStringAsync(); var dataFetchResult = JsonSerializer.Deserialize<DataFetchResult>(responseBody, new JsonSerializerOptions { PropertyNameCaseInsensitive = true }); return dataFetchResult; } static OrderItem? AsOrderItem(this string responseBody) { return JsonSerializer.Deserialize<OrderItem>(responseBody, new JsonSerializerOptions { PropertyNameCaseInsensitive = true }); } public static async Task<OrderItem?> GetOrderItemAsync(int id) { var response = await authorizedHttpClient.GetAsync($"{baseUrl}/data/OrderItem/{id}"); response.EnsureSuccessStatusCode(); var responseBody = await response.Content.ReadAsStringAsync(); return responseBody.AsOrderItem(); } public static async Task<OrderItem?> CreateOrderItemAsync(OrderItem orderItem) { var response = await authorizedHttpClient.PostAsync($"{baseUrl}/data/OrderItem", new StringContent(JsonSerializer.Serialize(orderItem), Encoding.UTF8, "application/json")); response.EnsureSuccessStatusCode(); var responseBody = await response.Content.ReadAsStringAsync(); return responseBody.AsOrderItem(); } public static async Task UpdateOrderItemAsync(OrderItem orderItem) { var response = await authorizedHttpClient.PutAsync($"{baseUrl}/data/OrderItem/{orderItem.Id}", new StringContent(JsonSerializer.Serialize(orderItem), Encoding.UTF8, "application/json")); response.EnsureSuccessStatusCode(); } public static async Task<bool> DeleteOrderItemAsync(int id) { try { var response = await authorizedHttpClient.DeleteAsync($"{baseUrl}/data/OrderItem/{id}"); response.EnsureSuccessStatusCode(); return true; } catch (Exception ex) { Debug.WriteLine(ex); return false; } } } }
DataService/Program.cs
C#
using DataService; using Microsoft.AspNetCore.Authentication.JwtBearer; using Microsoft.AspNetCore.Authorization; using Microsoft.EntityFrameworkCore; using Microsoft.IdentityModel.Tokens; using System.Linq.Dynamic.Core; using System.Reflection; using System.Text.Json.Nodes; using System.Xml.Serialization; var builder = WebApplication.CreateBuilder(args); string? connectionString = builder.Configuration.GetConnectionString("ConnectionString"); var publicKey = await GetKeycloakPublicKey(builder.Configuration["Jwt:KeycloakUrl"]!, builder.Configuration["Jwt:Realm"]!); builder.Services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme) .AddJwtBearer(options => { options.RequireHttpsMetadata = false; // Development only!! options.TokenValidationParameters = new TokenValidationParameters { ValidateIssuer = true, ValidIssuer = $"{builder.Configuration["Jwt:KeycloakUrl"]}/realms/{builder.Configuration["Jwt:Realm"]}", ValidateAudience = true, ValidAudience = builder.Configuration["Jwt:Audience"], ValidateLifetime = true, ValidateIssuerSigningKey = true, IssuerSigningKey = publicKey }; }); builder.Services.AddAuthorization(o => { o.AddPolicy("writers", p => p.RequireRealmRole("writers")); }); builder.Services.AddDbContext<DataServiceDbContext>(o => o.UseSqlServer(connectionString, options => { options.EnableRetryOnFailure(); })); var app = builder.Build(); app.UseAuthentication(); app.UseAuthorization(); // Make sure the database exists and is current using (var scope = app.Services.CreateScope()) { var dbContext = scope.ServiceProvider.GetRequiredService<DataServiceDbContext>(); dbContext.Database.Migrate(); } // Note that this endpoint is NOT configured to require authorization. For the demo, // this makes it possible to populate the database with test data without having to // authenticate first. In a real-world application, you would want to secure this endpoint. app.MapGet("/api/populateTestData", async (DataServiceDbContext dbContext) => { var assembly = Assembly.GetExecutingAssembly(); Console.WriteLine(String.Join("\n", assembly.GetManifestResourceNames())); var resourceName = assembly.GetManifestResourceNames().Single(str => str.EndsWith("order_items.xml")); var serializer = new XmlSerializer(typeof(List<OrderItem>)); using Stream? stream = assembly.GetManifestResourceStream(resourceName); if (stream is not null) { var items = (List<OrderItem>?)serializer.Deserialize(stream); if (items is not null) { dbContext.OrderItems.AddRange(items); await dbContext.SaveChangesAsync(); return Results.Ok("Data populated successfully"); } } return Results.NotFound("Error populating data"); }); // The following two endpoints are read-only, so they only require an authenticated user. 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 }); }).RequireAuthorization(); app.MapGet("/data/OrderItem/{id}", async (DataServiceDbContext dbContext, int id) => { var orderItem = await dbContext.OrderItems.FindAsync(id); if (orderItem is null) { return Results.NotFound(); } return Results.Ok(orderItem); }).RequireAuthorization(); // The following endpoints are read-write, so they require an authenticated user and // compliance with the "writers" policy. app.MapPost("/data/OrderItem", async (DataServiceDbContext dbContext, OrderItem orderItem) => { dbContext.OrderItems.Add(orderItem); await dbContext.SaveChangesAsync(); return Results.Created($"/data/OrderItem/{orderItem.Id}", orderItem); }).RequireAuthorization("writers"); 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(); }).RequireAuthorization("writers"); 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(); }).RequireAuthorization("writers"); app.Run(); static async Task<SecurityKey> GetKeycloakPublicKey(string keycloakUrl, string realm) { using (var httpClient = new HttpClient()) { var jwksUrl = $"{keycloakUrl}/realms/{realm}/protocol/openid-connect/certs"; var jwksJson = await httpClient.GetStringAsync(jwksUrl); var jwks = new JsonWebKeySet(jwksJson); return jwks.Keys[0]; } } public static class PolicyHelpers { public static void RequireRealmRole(this AuthorizationPolicyBuilder policy, string roleName) { policy.RequireAssertion(context => { var realmAccess = context.User.FindFirst("realm_access")?.Value; if (realmAccess == null) return false; var node = JsonNode.Parse(realmAccess); if (node == null || node["roles"] == null) return false; var array = node["roles"]!.AsArray(); return array.Select(r => r?.GetValue<string>()).Contains(roleName); }); } }
WinForms.Client/LoginForm.cs
C#
using DevExpress.XtraEditors; namespace WinForms.Client { public partial class LoginForm : DevExpress.XtraEditors.XtraForm { public LoginForm() { InitializeComponent(); } private async void loginButton_Click(object sender, EventArgs e) { if (string.IsNullOrEmpty(userNameEdit.Text) || string.IsNullOrEmpty(passwordEdit.Text)) { XtraMessageBox.Show("Please enter username and password", "Error", MessageBoxButtons.OK, MessageBoxIcon.Error); return; } if (await DataServiceClient.LogIn(userNameEdit.Text, passwordEdit.Text)) { this.DialogResult = DialogResult.OK; this.Close(); } else { XtraMessageBox.Show("Username or password are invalid, or a technical error occurred.", "Error", MessageBoxButtons.OK, MessageBoxIcon.Error); } } } }
WinForms.Client/MainForm.cs
C#
using DevExpress.XtraEditors; using DevExpress.XtraGrid.Views.Base; using DevExpress.XtraGrid.Views.Grid; using DevExpress.XtraSplashScreen; using System.Drawing.Text; namespace WinForms.Client { public partial class MainForm : XtraForm { public MainForm() { InitializeComponent(); } class CustomOverlayTextPainter(string text) : OverlayWindowPainterBase { static readonly Font font = new("Tahoma", 16, FontStyle.Bold); protected override void Draw(OverlayWindowCustomDrawContext context) { var cache = context.DrawArgs.Cache; cache.TextRenderingHint = TextRenderingHint.AntiAlias; context.DrawBackground(); var bounds = context.DrawArgs.Bounds; var midX = bounds.Left + bounds.Width / 2; var midY = bounds.Top + bounds.Height / 2; SizeF textSize = cache.CalcTextSize(text, font); cache.DrawString(text, font, Brushes.Black, new PointF(midX - textSize.Width / 2, midY + 30)); context.Handled = true; } } class CustomOverlayImagePainter : OverlayImagePainter { public CustomOverlayImagePainter(Image image, Action clickAction) : base(image, clickAction: clickAction) { } protected override Rectangle CalcImageBounds(OverlayLayeredWindowObjectInfoArgs drawArgs) { var midX = drawArgs.Bounds.Left + drawArgs.Bounds.Width / 2; var midY = drawArgs.Bounds.Top + drawArgs.Bounds.Height / 2; return new Rectangle(new Point(midX - Image.Size.Width / 2, midY - Image.Size.Height / 2 - 50), Image.Size); } } IOverlaySplashScreenHandle? overlayHandle; protected override void OnShown(EventArgs e) { base.OnShown(e); ShowOverlay(); } private void ShowOverlay() { overlayHandle = SplashScreenManager.ShowOverlayForm(this, opacity: 200, customPainter: new OverlayWindowCompositePainter( new CustomOverlayTextPainter("Click the lock to log in"), new CustomOverlayImagePainter(svgImageCollection.GetImage(0), LogIn) )); } private void LogIn() { if (!DataServiceClient.LoggedIn) { var loginForm = new LoginForm(); loginForm.ShowDialog(); if (DataServiceClient.LoggedIn) { EvaluateRoles(); logOutItem.Caption = $"Log out {DataServiceClient.Name}{(userIsWriter ? " (Writer)" : "")}"; if (overlayHandle is not null) SplashScreenManager.CloseOverlayForm(overlayHandle); Invoke(new Action(() => { gridControl.DataSource = virtualServerModeSource; })); } } } private void logOutItem_ItemClick(object sender, DevExpress.XtraBars.ItemClickEventArgs e) { if (DataServiceClient.LoggedIn) { EvaluateRoles(); gridControl.DataSource = null; logOutItem.Caption = "Log Out"; DataServiceClient.LogOut(); ShowOverlay(); } } private bool userIsWriter = false; private void EvaluateRoles() { if (DataServiceClient.LoggedIn) { if (DataServiceClient.UserHasRole("writers")) { userIsWriter = true; addItemButton.Enabled = true; deleteItemButton.Enabled = true; } else { userIsWriter = false; addItemButton.Enabled = false; deleteItemButton.Enabled = false; } } else { userIsWriter = false; addItemButton.Enabled = false; deleteItemButton.Enabled = false; } } private VirtualServerModeDataLoader? loader; private void VirtualServerModeSource_ConfigurationChanged(object? sender, DevExpress.Data.VirtualServerModeRowsEventArgs e) { loader = new VirtualServerModeDataLoader(e.ConfigurationInfo); e.RowsTask = loader.GetRowsAsync(e); } private void VirtualServerModeSource_MoreRows(object? sender, DevExpress.Data.VirtualServerModeRowsEventArgs e) { if (loader is not null) { e.RowsTask = loader.GetRowsAsync(e); } } private void NotAWriterError() { XtraMessageBox.Show("You are not authorized to edit items.", "Error", MessageBoxButtons.OK, MessageBoxIcon.Error); } private async void gridView1_DoubleClick(object sender, EventArgs e) { if (!userIsWriter) { NotAWriterError(); return; } 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(); } } } } private async void addItemButton_ItemClick(object sender, DevExpress.XtraBars.ItemClickEventArgs e) { if (!userIsWriter) { NotAWriterError(); return; } if (gridControl.FocusedView is ColumnView view) { var createResult = EditForm.CreateItem(); if (createResult.changesSaved) { await DataServiceClient.CreateOrderItemAsync(createResult.item!); view.RefreshData(); } } } private async void deleteItemButton_ItemClick(object sender, DevExpress.XtraBars.ItemClickEventArgs e) { if (!userIsWriter) { NotAWriterError(); return; } if (gridControl.FocusedView is ColumnView view && view.GetFocusedRow() is OrderItem orderItem) { await DataServiceClient.DeleteOrderItemAsync(orderItem.Id); view.RefreshData(); } } } }

Disclaimer: The information provided on DevExpress.com and affiliated web properties (including the DevExpress Support Center) is provided "as is" without warranty of any kind. Developer Express Inc disclaims all warranties, either express or implied, including the warranties of merchantability and fitness for a particular purpose. Please refer to the DevExpress.com Website Terms of Use for more information in this regard.

Confidential Information: Developer Express Inc does not wish to receive, will not act to procure, nor will it solicit, confidential or proprietary materials and information from you through the DevExpress Support Center or its web properties. Any and all materials or information divulged during chats, email communications, online discussions, Support Center tickets, or made available to Developer Express Inc in any manner will be deemed NOT to be confidential by Developer Express Inc. Please refer to the DevExpress.com Website Terms of Use for more information in this regard.