원클릭으로
wpf-settings-form
Pattern for building single-column settings forms in WPF with validation and read-only/editable fields
Codex 또는 Claude로 설치 이 Prompt를 복사해 Codex, Claude 또는 다른 어시스턴트에 붙여 넣으면 Skill 페이지를 검토하고 설치를 진행할 수 있습니다.
메뉴
Pattern for building single-column settings forms in WPF with validation and read-only/editable fields
Codex 또는 Claude로 설치 이 Prompt를 복사해 Codex, Claude 또는 다른 어시스턴트에 붙여 넣으면 Skill 페이지를 검토하고 설치를 진행할 수 있습니다.
SOC 직업 분류 기준
| name | wpf-settings-form |
| description | Pattern for building single-column settings forms in WPF with validation and read-only/editable fields |
| domain | desktop-ui |
| confidence | high |
| source | earned |
When building WPF applications that need configuration UI, a common requirement is a settings form that displays configuration values with some editable and some read-only. The form should validate edits, handle save/cancel/reset actions, and integrate with existing configuration services.
ElBruno.OllamaMonitor Settings Window demonstrates this pattern with 9 configuration keys (2 editable, 7 read-only).
Use a Grid with 4 rows for consistent layout:
<Grid Margin="20">
<Grid.RowDefinitions>
<RowDefinition Height="Auto"/> <!-- Header -->
<RowDefinition Height="*"/> <!-- Scrollable Form -->
<RowDefinition Height="Auto"/> <!-- Footer Warning -->
<RowDefinition Height="Auto"/> <!-- Action Buttons -->
</Grid.RowDefinitions>
</Grid>
Define styles for consistency across fields:
<Window.Resources>
<Style x:Key="SectionHeaderStyle" TargetType="TextBlock">
<Setter Property="FontSize" Value="14"/>
<Setter Property="FontWeight" Value="SemiBold"/>
<Setter Property="Foreground" Value="#FF374151"/>
<Setter Property="Margin" Value="0,16,0,8"/>
</Style>
<Style x:Key="FieldLabelStyle" TargetType="TextBlock">
<Setter Property="FontSize" Value="12"/>
<Setter Property="Foreground" Value="#FF6B7280"/>
<Setter Property="Margin" Value="0,8,0,4"/>
</Style>
<Style x:Key="EditableFieldStyle" TargetType="TextBox">
<Setter Property="Padding" Value="8"/>
<Setter Property="FontSize" Value="12"/>
<Setter Property="BorderBrush" Value="#FFD1D5DB"/>
<Setter Property="BorderThickness" Value="1"/>
</Style>
<Style x:Key="ReadOnlyFieldStyle" TargetType="TextBox">
<Setter Property="Padding" Value="8"/>
<Setter Property="FontSize" Value="12"/>
<Setter Property="BorderBrush" Value="#FFE5E7EB"/>
<Setter Property="Background" Value="#FFF9FAFB"/>
<Setter Property="IsReadOnly" Value="True"/>
</Style>
</Window.Resources>
Each editable field should have an error label below it:
<TextBlock Style="{StaticResource FieldLabelStyle}" Text="Endpoint *"/>
<TextBox x:Name="EndpointTextBox"
Style="{StaticResource EditableFieldStyle}"
ToolTip="The base URL of your Ollama API endpoint"/>
<TextBlock x:Name="EndpointErrorText"
Foreground="#FFDC2626"
FontSize="11"
Margin="0,2,0,0"
Visibility="Collapsed"/>
Use a Border for restart/warning notices:
<Border Grid.Row="2"
Background="#FFFFFBEB"
BorderBrush="#FFFBBF24"
BorderThickness="1"
CornerRadius="4"
Padding="12"
Margin="0,12,0,12">
<StackPanel Orientation="Horizontal">
<TextBlock Text="⚠" FontSize="14" Foreground="#FFFBBF24" Margin="0,0,8,0"/>
<TextBlock Text="Restart required for changes to take effect."
FontSize="11"
Foreground="#FF92400E"/>
</StackPanel>
</Border>
Right-aligned buttons with primary styling:
<StackPanel Grid.Row="3" Orientation="Horizontal" HorizontalAlignment="Right">
<Button x:Name="ResetButton" Content="Reset to Defaults" Width="130" Height="32"
Margin="0,0,8,0" Click="OnResetClicked"/>
<Button x:Name="CancelButton" Content="Cancel" Width="80" Height="32"
Margin="0,0,8,0" Click="OnCancelClicked"/>
<Button x:Name="SaveButton" Content="Save" Width="80" Height="32"
Click="OnSaveClicked" IsDefault="True"
Background="#FF3B82F6" Foreground="White"/>
</StackPanel>
Constructor takes service dependencies, async load on Loaded event:
public SettingsWindow(AppSettingsService settingsService)
{
_settingsService = settingsService;
InitializeComponent();
Loaded += async (_, _) => await LoadSettingsAsync();
}
private async Task LoadSettingsAsync()
{
var settings = await _settingsService.LoadAsync(CancellationToken.None);
EndpointTextBox.Text = settings.Endpoint;
RefreshIntervalTextBox.Text = settings.RefreshIntervalSeconds.ToString();
StartMinimizedCheckBox.IsChecked = settings.StartMinimizedToTray;
// ... other fields
}
Always reload from disk before saving to prevent overwriting concurrent changes:
private async void OnSaveClicked(object sender, RoutedEventArgs e)
{
ClearErrors();
// Validate editable fields
var validation = ValidateEndpoint(EndpointTextBox.Text.Trim());
if (!validation.ok)
{
ShowError(EndpointErrorText, validation.error!);
return;
}
SaveButton.IsEnabled = false;
SaveButton.Content = "Saving...";
// CRITICAL: Reload from disk first (last-write-wins)
var current = await _settingsService.LoadAsync(CancellationToken.None);
// Apply user edits to reloaded object
var updated = current with
{
Endpoint = EndpointTextBox.Text.Trim(),
RefreshIntervalSeconds = int.Parse(RefreshIntervalTextBox.Text.Trim())
};
await _settingsService.SaveAsync(updated, CancellationToken.None);
Hide();
}
Use tuples to avoid exception-based control flow:
private static (bool ok, string? error) ValidateEndpoint(string endpoint)
{
if (string.IsNullOrWhiteSpace(endpoint))
return (false, "Endpoint cannot be empty.");
if (!Uri.TryCreate(endpoint, UriKind.Absolute, out var uri))
return (false, "Endpoint must be a valid URI.");
if (uri.Scheme != "http" && uri.Scheme != "https")
return (false, "Endpoint must use http or https.");
return (true, null);
}
private void ShowError(TextBlock errorLabel, string message)
{
errorLabel.Text = message;
errorLabel.Visibility = Visibility.Visible;
}
private void ClearErrors()
{
EndpointErrorText.Visibility = Visibility.Collapsed;
RefreshIntervalErrorText.Visibility = Visibility.Collapsed;
}
Reset does NOT auto-close the window (user may want to review defaults):
private async void OnResetClicked(object sender, RoutedEventArgs e)
{
var result = System.Windows.MessageBox.Show(
"Are you sure you want to reset all settings to defaults?",
"Reset Settings",
System.Windows.MessageBoxButton.YesNo,
System.Windows.MessageBoxImage.Question);
if (result != System.Windows.MessageBoxResult.Yes)
return;
ResetButton.IsEnabled = false;
ResetButton.Content = "Resetting...";
await _settingsService.ResetAsync(CancellationToken.None);
await LoadSettingsAsync(); // Reload form fields from new defaults
}
Use PrepareForExit flag to allow reopening without re-instantiation:
private bool _allowClose;
public void PrepareForExit() => _allowClose = true;
protected override void OnClosing(CancelEventArgs e)
{
if (!_allowClose)
{
e.Cancel = true;
Hide();
return;
}
base.OnClosing(e);
}
File: src/ElBruno.OllamaMonitor/Windows/SettingsWindow.xaml
File: src/ElBruno.OllamaMonitor/Windows/SettingsWindow.xaml.cs
(bool ok, string? error) for clean control flowSystem.Windows.MessageBox (fully qualify to avoid ambiguity)Tray menu integration:
App.xaml.cs lifecycle:
Service layer integration: