diff --git a/YKanBan.Tests/Conventions/CodeBehindConventionTests.cs b/YKanBan.Tests/Conventions/CodeBehindConventionTests.cs
new file mode 100644
index 0000000..28390c0
--- /dev/null
+++ b/YKanBan.Tests/Conventions/CodeBehindConventionTests.cs
@@ -0,0 +1,60 @@
+using System.Runtime.CompilerServices;
+using System.Text.RegularExpressions;
+
+namespace YKanBan.Tests.Conventions;
+
+///
+/// Source-level guards for code-behind conventions (plan ยง8.1): inside control,
+/// window and application code-behind a bare Resources.Key resolves to
+/// the StyledElement.Resources instance property, so the generated class
+/// must be written as YKanBan.Resources (or through a using alias).
+///
+[TestClass]
+public partial class CodeBehindConventionTests
+{
+ /// A Resources. member access not preceded by a qualifier or identifier character.
+ [GeneratedRegex(@"(?
+ /// Locates the application project's source folder from this file's compile-time path.
+ ///
+ /// Filled in by the compiler.
+ /// The YKanBan project folder.
+ private static string ApplicationSourceFolder([CallerFilePath] string thisFile = "")
+ {
+ DirectoryInfo? directory = new FileInfo(thisFile).Directory;
+ while (directory is not null && !File.Exists(Path.Combine(directory.FullName, "YKanBan", "YKanBan.csproj")))
+ {
+ directory = directory.Parent;
+ }
+
+ Assert.IsNotNull(directory, "Repository root not found from the test source path.");
+ return Path.Combine(directory.FullName, "YKanBan");
+ }
+
+ [TestMethod]
+ public void CodeBehindNeverUsesBareResourcesClassName()
+ {
+ string root = ApplicationSourceFolder();
+ string[] files = Directory.GetFiles(root, "*.axaml.cs", SearchOption.AllDirectories)
+ .Where(path => !path.Split(Path.DirectorySeparatorChar).Any(part => part is "obj" or "bin"))
+ .ToArray();
+ Assert.IsTrue(files.Length > 0, "No code-behind files found.");
+
+ var offenders = new List();
+ foreach (string file in files)
+ {
+ string[] lines = File.ReadAllLines(file);
+ for (int index = 0; index < lines.Length; index++)
+ {
+ if (BareResourcesAccess().IsMatch(lines[index]))
+ {
+ offenders.Add($"{Path.GetRelativePath(root, file)}:{index + 1}: {lines[index].Trim()}");
+ }
+ }
+ }
+
+ Assert.AreEqual(0, offenders.Count, "Use YKanBan.Resources in code-behind:" + Environment.NewLine + string.Join(Environment.NewLine, offenders));
+ }
+}
diff --git a/YKanBan/App.axaml.cs b/YKanBan/App.axaml.cs
index 1977aec..dae177c 100644
--- a/YKanBan/App.axaml.cs
+++ b/YKanBan/App.axaml.cs
@@ -26,6 +26,12 @@ public partial class App : Application
///
public static string StartupLanguage { get; private set; } = AppConfig.DefaultLanguage;
+ ///
+ /// Gets the flow direction of the startup culture. FlowDirection does not
+ /// inherit across windows, so the main window and every modal window apply it.
+ ///
+ public static FlowDirection UiFlowDirection { get; private set; } = FlowDirection.LeftToRight;
+
public override void Initialize()
{
AvaloniaXamlLoader.Load(this);
@@ -43,19 +49,20 @@ public partial class App : Application
CultureInfo culture = ResolveCulture(Services.Config.Language);
StartupLanguage = Services.Config.Language;
Resx.Culture = culture;
+
+ // RTL detection drives the flow direction of every window; the two
+ // supported languages are LTR, but the infrastructure stays in
+ // place for future languages.
+ UiFlowDirection = culture.TextInfo.IsRightToLeft
+ ? FlowDirection.RightToLeft
+ : FlowDirection.LeftToRight;
ApplyTheme(Services.Config.Theme);
var viewModel = new MainWindowViewModel(Services, desktop.Args ?? []);
desktop.MainWindow = new MainWindow
{
DataContext = viewModel,
-
- // RTL detection drives the whole window flow direction; the two
- // supported languages are LTR, but the infrastructure stays in
- // place for future languages.
- FlowDirection = culture.TextInfo.IsRightToLeft
- ? FlowDirection.RightToLeft
- : FlowDirection.LeftToRight,
+ FlowDirection = UiFlowDirection,
};
}
diff --git a/YKanBan/Views/MainWindow.axaml.cs b/YKanBan/Views/MainWindow.axaml.cs
index 6cbaf8f..0f19084 100644
--- a/YKanBan/Views/MainWindow.axaml.cs
+++ b/YKanBan/Views/MainWindow.axaml.cs
@@ -37,7 +37,7 @@ public partial class MainWindow : Window
{
DataContext = new SettingsDialogViewModel(App.Services.Config, App.StartupLanguage),
};
- await dialog.ShowDialog(this);
+ await ShowModalAsync(dialog);
}
/// Shows the about dialog centered on this window.
@@ -47,6 +47,18 @@ public partial class MainWindow : Window
{
DataContext = new AboutDialogViewModel(),
};
- await dialog.ShowDialog(this);
+ await ShowModalAsync(dialog);
+ }
+
+ ///
+ /// Shows a modal window owned by this window. Every modal goes through here:
+ /// FlowDirection does not inherit across windows, so it is applied explicitly.
+ ///
+ /// The modal window to show.
+ /// A task completing when the dialog closes.
+ private Task ShowModalAsync(Window dialog)
+ {
+ dialog.FlowDirection = App.UiFlowDirection;
+ return dialog.ShowDialog(this);
}
}