Build a Configurable Admin Panel in WPF Using Toolkit Plus PropertyGrid

Internal tools often ship with bare-minimum config screens and then rot: fields scatter across dialogs, validation is inconsistent, and adding new settings means weeks of UI plumbing. WPF PropertyGrid from Xceed’s WPF Toolkit Plus is the silent time-saver that flips this script. You bind a POCO settings object, get production-ready editors instantly, then layer in custom editors, attributes, and validation as needed. Result: a consistent, branded, MVVM-friendly admin panel without a mountain of XAML.

Build a Config Panel Fast

Getting started: install and bind a Settings object

Install and namespaces

  • Add the Toolkit Plus package to your WPF project.
  • In XAML, import the namespace:

xmlns:xcad="<http://schemas.xceed.com/wpf/xaml/toolkit>"

Basic binding to Settings in MVVM

Create a Settings POCO and expose it on a SettingsViewModel. Bind PropertyGrid.SelectedObject to your Settings instance—no templates required.

Code: Settings model (baseline)

public class AppSettings
{
  public string Environment { get; set; } = "Production";
  public int RetryCount { get; set; } = 3;
  public bool EnableAuditTrail { get; set; } = true;
  public TimeSpan Timeout { get; set; } = TimeSpan.FromSeconds(30);
  public string ThemeColor { get; set; } = "#FFAA5500"; // brand accent
  public string ReportsFolder { get; set; } = "C:\\\\Reports";
}

Code: ViewModel

public class SettingsViewModel : INotifyPropertyChanged
{
  public AppSettings Settings { get; } = new();
  public ICommand SaveCommand { get; }
  public ICommand LoadCommand { get; }

  public SettingsViewModel()
  {
    SaveCommand = new RelayCommand(_ => Save());
    LoadCommand = new RelayCommand(_ => Load());
  }

  // Implement INotifyPropertyChanged, Save, Load below
}

Code: XAML (PropertyGrid)

<Grid>
  <Grid.RowDefinitions>
    <RowDefinition Height="*"/>
    <RowDefinition Height="Auto"/>
  </Grid.RowDefinitions>

  <xcad:PropertyGrid
    SelectedObject="{Binding Settings}"
    AutoGenerateProperties="True"
    NameColumnWidth="240"
    HelpVisible="True"
    DescriptionVisibility="Visible"
    IsCategorized="True" />

  <StackPanel Grid.Row="1" Orientation="Horizontal" HorizontalAlignment="Right" Margin="0,12,0,0">
    <Button Content="Load" Command="{Binding LoadCommand}" Margin="0,0,8,0"/>
    <Button Content="Save" Command="{Binding SaveCommand}" />
  </StackPanel>
</Grid>


Custom editors and attributes that feel “editor-quality”

Use .NET attributes to group, label, and describe settings. When you need richer input (dropdowns, numeric spinners, color pickers, file pickers), swap editors cleanly via EditorAttribute or editor mappings.

Categorize and describe with attributes

Code: Settings model with attributes

using System.ComponentModel;

public class AppSettings
{
  [Category("General")]
  [DisplayName("Environment")]
  [Description("Target environment for API calls.")]
  public string Environment { get; set; } = "Production";

  [Category("General")]
  [DisplayName("Retry Count")]
  [Description("Number of retry attempts for transient failures.")]
  public int RetryCount { get; set; } = 3;

  [Category("Security")]
  [DisplayName("Enable Audit Trail")]
  [Description("Record configuration changes for compliance.")]
  public bool EnableAuditTrail { get; set; } = true;

  [Category("Performance")]
  [DisplayName("Timeout")]
  [Description("Request timeout duration.")]
  public TimeSpan Timeout { get; set; } = TimeSpan.FromSeconds(30);

  [Category("Branding")]
  [DisplayName("Theme Color")]
  [Description("Accent color for the application theme.")]
  public string ThemeColor { get; set; } = "#FFAA5500";

  [Category("Reports")]
  [DisplayName("Reports Folder")]
  [Description("Directory where generated reports are stored.")]
  public string ReportsFolder { get; set; } = "C:\\\\Reports";
}

Swap in dropdowns, numeric spinners, color pickers, and file pickers

Approach A: Type-based editor mapping in XAML (keeps models clean).

Code: Editor mapping in XAML

<xcad:PropertyGrid SelectedObject="{Binding Settings}" AutoGenerateProperties="True" IsCategorized="True">
  <xcad:PropertyGrid.EditorDefinitions>

    <!-- Environment: ComboBox with fixed options -->
    <xcad:EditorTemplateDefinition>
      <xcad:EditorTemplateDefinition.TargetProperties>
        <xcad:TargetPropertyDefinition PropertyName="Environment"/>
      </xcad:EditorTemplateDefinition.TargetProperties>
      <xcad:EditorTemplateDefinition.EditingTemplate>
        <DataTemplate>
          <ComboBox SelectedItem="{Binding Value, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}"
                    ItemsSource="{Binding DataContext.Environments, RelativeSource={RelativeSource AncestorType=xcad:PropertyGrid}}" />
        </DataTemplate>
      </xcad:EditorTemplateDefinition.EditingTemplate>
    </xcad:EditorTemplateDefinition>

    <!-- RetryCount: NumericUpDown -->
    <xcad:EditorTemplateDefinition>
      <xcad:EditorTemplateDefinition.TargetProperties>
        <xcad:TargetPropertyDefinition PropertyName="RetryCount"/>
      </xcad:EditorTemplateDefinition.TargetProperties>
      <xcad:EditorTemplateDefinition.EditingTemplate>
        <DataTemplate>
          <xcad:IntegerUpDown Minimum="0" Maximum="10"
             Value="{Binding Value, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}" />
        </DataTemplate>
      </xcad:EditorTemplateDefinition.EditingTemplate>
    </xcad:EditorTemplateDefinition>

    <!-- ThemeColor: ColorPicker -->
    <xcad:EditorTemplateDefinition>
      <xcad:EditorTemplateDefinition.TargetProperties>
        <xcad:TargetPropertyDefinition PropertyName="ThemeColor"/>
      </xcad:EditorTemplateDefinition.TargetProperties>
      <xcad:EditorTemplateDefinition.EditingTemplate>
        <DataTemplate>
          <xcad:ColorPicker
            SelectedColor="{Binding Value, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}" />
        </DataTemplate>
      </xcad:EditorTemplateDefinition.EditingTemplate>
    </xcad:EditorTemplateDefinition>

    <!-- ReportsFolder: file picker -->
    <xcad:EditorTemplateDefinition>
      <xcad:EditorTemplateDefinition.TargetProperties>
        <xcad:TargetPropertyDefinition PropertyName="ReportsFolder"/>
      </xcad:EditorTemplateDefinition.TargetProperties>
      <xcad:EditorTemplateDefinition.EditingTemplate>
        <DataTemplate>
          <DockPanel>
            <TextBox Text="{Binding Value, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}" MinWidth="260" />
            <Button Content="Browse..." Margin="8,0,0,0" Command="{Binding DataContext.BrowseFolderCommand, RelativeSource={RelativeSource AncestorType=xcad:PropertyGrid}}" />
          </DockPanel>
        </DataTemplate>
      </xcad:EditorTemplateDefinition.EditingTemplate>
    </xcad:EditorTemplateDefinition>

  </xcad:PropertyGrid.EditorDefinitions>
</xcad:PropertyGrid>

Code: ViewModel extras for editors

public ObservableCollection<string> Environments { get; } =
  new(new[] { "Development", "Staging", "Production" });

public ICommand BrowseFolderCommand => new RelayCommand(_ =>
{
  var dlg = new Microsoft.Win32.OpenFileDialog
  {
    CheckFileExists = false,
    ValidateNames = false,
    FileName = "Select Folder"
  };
  if (dlg.ShowDialog() == true)
  {
    // Get folder from selected path
    var folder = System.IO.Path.GetDirectoryName(dlg.FileName);
    if (!string.IsNullOrEmpty(folder))
      Settings.ReportsFolder = folder;
  }
});

Approach B: Attribute-driven editors (when you prefer annotating the model)

Use EditorAttribute to associate specific editors with properties. If you maintain a shared “Settings” assembly, keep it UI-agnostic and prefer XAML mapping (Approach A).


Validation and persistence that don’t fight MVVM

Live validation with INotifyDataErrorInfo

Implement INotifyDataErrorInfo on the ViewModel to surface inline validation (for example minimum timeouts, folder existence). PropertyGrid will display errors without modal dialogs.

Code: INotifyDataErrorInfo pattern

public partial class SettingsViewModel : INotifyPropertyChanged, INotifyDataErrorInfo
{
  private readonly Dictionary<string, List<string>> _errors = new();

  public bool HasErrors => _errors.Count > 0;
  public event EventHandler<DataErrorsChangedEventArgs> ErrorsChanged;

  public IEnumerable GetErrors(string propertyName)
    => propertyName != null && _errors.TryGetValue(propertyName, out var list) ? list : null;

  private void Validate()
  {
    ClearErrors(nameof(Settings.Timeout));
    if (Settings.Timeout < TimeSpan.FromSeconds(5))
      AddError(nameof(Settings.Timeout), "Timeout must be at least 5 seconds.");

    ClearErrors(nameof(Settings.ReportsFolder));
    if (string.IsNullOrWhiteSpace(Settings.ReportsFolder) || !Directory.Exists(Settings.ReportsFolder))
      AddError(nameof(Settings.ReportsFolder), "Folder must exist.");

    ClearErrors(nameof(Settings.RetryCount));
    if (Settings.RetryCount is < 0 or > 10)
      AddError(nameof(Settings.RetryCount), "RetryCount must be between 0 and 10.");
  }

  private void AddError(string prop, string error)
  {
    if (!_errors.TryGetValue(prop, out var list)) _errors[prop] = list = new List<string>();
    if (!list.Contains(error)) list.Add(error);
    ErrorsChanged?.Invoke(this, new DataErrorsChangedEventArgs(prop));
  }

  private void ClearErrors(string prop)
  {
    if (_errors.Remove(prop)) ErrorsChanged?.Invoke(this, new DataErrorsChangedEventArgs(prop));
  }
}

Wire validation to property changes (for example via setter notifications or a timer/debounce if needed).

Persist settings with System.Text.Json

Code: Save/Load helpers

using System.Text.Json;

private static readonly JsonSerializerOptions JsonOpts = new()
{
  WriteIndented = true
};

private string SettingsFilePath =>
  Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData), "MyApp", "settings.json");

private void Save()
{
  Directory.CreateDirectory(Path.GetDirectoryName(SettingsFilePath)!);
  var json = JsonSerializer.Serialize(Settings, JsonOpts);
  File.WriteAllText(SettingsFilePath, json);
}

private void Load()
{
  if (!File.Exists(SettingsFilePath)) return;
  var json = File.ReadAllText(SettingsFilePath);
  var loaded = JsonSerializer.Deserialize<AppSettings>(json) ?? new AppSettings();

  // shallow copy to preserve SelectedObject binding
  Settings.Environment = loaded.Environment;
  Settings.RetryCount = loaded.RetryCount;
  Settings.EnableAuditTrail = loaded.EnableAuditTrail;
  Settings.Timeout = loaded.Timeout;
  Settings.ThemeColor = loaded.ThemeColor;
  Settings.ReportsFolder = loaded.ReportsFolder;

  OnPropertyChanged(nameof(Settings));
  Validate();
}

Call Load() on startup, Save() on user action or exit.


Theming: make it brand-consistent and accessible

Pair WPF PropertyGrid with Pro Themes for WPF so your admin panel inherits consistent focus states, contrast, and typography—especially important in dense settings UIs. It keeps your internal tools visually aligned with your product and cuts the time spent on polishing editor visuals.

  • Explore Pro Themes for WPF to apply a cohesive, accessible theme across all controls.
  • If your configs affect data behaviors, consider DataGrid for WPF for high-performance reviews and batch edits.


Final result

What you get: an admin/config panel where new properties appear instantly, categorized and labeled, with sensible default editors and inline validation. Add a property to your POCO, restart, and it’s live—no custom dialog work.

GIF-style walkthrough ideas

  • Add a new property to AppSettings, rebuild, and watch it appear in PropertyGrid with a default editor.
  • Change Environment via dropdown; validation overlays show live messages when values go out of range.
  • Click Save; a toast or inline message confirms settings persisted to JSON.


Paste-ready snippet: minimal PropertyGrid page

Code: Minimal page XAML

<Page
  xmlns="<http://schemas.microsoft.com/winfx/2006/xaml/presentation>"
  xmlns:x="<http://schemas.microsoft.com/winfx/2006/xaml>"
  xmlns:xcad="<http://schemas.xceed.com/wpf/xaml/toolkit>">
  <Grid Margin="16">
    <Grid.RowDefinitions>
      <RowDefinition Height="*"/>
      <RowDefinition Height="Auto"/>
    </Grid.RowDefinitions>

    <xcad:PropertyGrid
      SelectedObject="{Binding Settings}"
      AutoGenerateProperties="True"
      IsCategorized="True"
      NameColumnWidth="240"
      DescriptionVisibility="Visible"
      HelpVisible="True"/>

    <StackPanel Grid.Row="1" Orientation="Horizontal" HorizontalAlignment="Right" Margin="0,12,0,0">
      <Button Content="Load" Command="{Binding LoadCommand}" Margin="0,0,8,0"/>
      <Button Content="Save" Command="{Binding SaveCommand}"/>
    </StackPanel>
  </Grid>
</Page>


Internal links and related components

  • Pro Themes for WPF: apply consistent, accessible styling across all editors.
  • DataGrid for WPF: if configuration affects grid behavior (filters, formats, columns), pair with a fast, virtualized grid for admin views.
  • Apoyo: https://xceed.com/support/

Try It for yourself!