feat: apply ui flow direction to modal windows and guard code-behind resources usage
This commit is contained in:
1 parent
6743e1572e
commit
433f204c81
3 files changed
+88
-9
No files matched your search
@@ -0,0 +1,60 @@
|
|||||||
|
using System.Runtime.CompilerServices;
|
||||||
|
using System.Text.RegularExpressions;
|
||||||
|
|
||||||
|
namespace YKanBan.Tests.Conventions;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Source-level guards for code-behind conventions (plan §8.1): inside control,
|
||||||
|
/// window and application code-behind a bare <c>Resources.Key</c> resolves to
|
||||||
|
/// the <c>StyledElement.Resources</c> instance property, so the generated class
|
||||||
|
/// must be written as <c>YKanBan.Resources</c> (or through a using alias).
|
||||||
|
/// </summary>
|
||||||
|
[TestClass]
|
||||||
|
public partial class CodeBehindConventionTests
|
||||||
|
{
|
||||||
|
/// <summary>A <c>Resources.</c> member access not preceded by a qualifier or identifier character.</summary>
|
||||||
|
[GeneratedRegex(@"(?<![\w.])Resources\.")]
|
||||||
|
private static partial Regex BareResourcesAccess();
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Locates the application project's source folder from this file's compile-time path.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="thisFile">Filled in by the compiler.</param>
|
||||||
|
/// <returns>The YKanBan project folder.</returns>
|
||||||
|
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<string>();
|
||||||
|
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));
|
||||||
|
}
|
||||||
|
}
|
||||||
+14
-7
@@ -26,6 +26,12 @@ public partial class App : Application
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public static string StartupLanguage { get; private set; } = AppConfig.DefaultLanguage;
|
public static string StartupLanguage { get; private set; } = AppConfig.DefaultLanguage;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets the flow direction of the startup culture. FlowDirection does not
|
||||||
|
/// inherit across windows, so the main window and every modal window apply it.
|
||||||
|
/// </summary>
|
||||||
|
public static FlowDirection UiFlowDirection { get; private set; } = FlowDirection.LeftToRight;
|
||||||
|
|
||||||
public override void Initialize()
|
public override void Initialize()
|
||||||
{
|
{
|
||||||
AvaloniaXamlLoader.Load(this);
|
AvaloniaXamlLoader.Load(this);
|
||||||
@@ -43,19 +49,20 @@ public partial class App : Application
|
|||||||
CultureInfo culture = ResolveCulture(Services.Config.Language);
|
CultureInfo culture = ResolveCulture(Services.Config.Language);
|
||||||
StartupLanguage = Services.Config.Language;
|
StartupLanguage = Services.Config.Language;
|
||||||
Resx.Culture = culture;
|
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);
|
ApplyTheme(Services.Config.Theme);
|
||||||
|
|
||||||
var viewModel = new MainWindowViewModel(Services, desktop.Args ?? []);
|
var viewModel = new MainWindowViewModel(Services, desktop.Args ?? []);
|
||||||
desktop.MainWindow = new MainWindow
|
desktop.MainWindow = new MainWindow
|
||||||
{
|
{
|
||||||
DataContext = viewModel,
|
DataContext = viewModel,
|
||||||
|
FlowDirection = UiFlowDirection,
|
||||||
// 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,
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -37,7 +37,7 @@ public partial class MainWindow : Window
|
|||||||
{
|
{
|
||||||
DataContext = new SettingsDialogViewModel(App.Services.Config, App.StartupLanguage),
|
DataContext = new SettingsDialogViewModel(App.Services.Config, App.StartupLanguage),
|
||||||
};
|
};
|
||||||
await dialog.ShowDialog(this);
|
await ShowModalAsync(dialog);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>Shows the about dialog centered on this window.</summary>
|
/// <summary>Shows the about dialog centered on this window.</summary>
|
||||||
@@ -47,6 +47,18 @@ public partial class MainWindow : Window
|
|||||||
{
|
{
|
||||||
DataContext = new AboutDialogViewModel(),
|
DataContext = new AboutDialogViewModel(),
|
||||||
};
|
};
|
||||||
await dialog.ShowDialog(this);
|
await ShowModalAsync(dialog);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Shows a modal window owned by this window. Every modal goes through here:
|
||||||
|
/// FlowDirection does not inherit across windows, so it is applied explicitly.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="dialog">The modal window to show.</param>
|
||||||
|
/// <returns>A task completing when the dialog closes.</returns>
|
||||||
|
private Task ShowModalAsync(Window dialog)
|
||||||
|
{
|
||||||
|
dialog.FlowDirection = App.UiFlowDirection;
|
||||||
|
return dialog.ShowDialog(this);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Reference in new issue
Block a user