之前已经实现了一个简单Popup,但是没有实现超链接。现在我们有了WebView了,就可以开始魔改Hyperlink了,下一例子给出使用例子。
NavigationService.cs类,这个类可以替换跳转等。
public static class NavigationService
{
private sealed class Relation
{
public object? Host;
public object? Parent;
}
private static readonly ConcurrentDictionary<(Assembly assembly, string pageName), Type?> cache = [];
private static readonly ConditionalWeakTable<object, Relation> relationMap = [];
public static void Navigate(object current, string pageAxaml, bool fNewWindow = true, params object?[] ctorArgs)
{
try
{
if (current is null)
return;
var hostWindow = ResolveHostWindow(current);
if (hostWindow is null)
return;
var ownerWindow = ResolveOwnerWindowForModal(current) ?? hostWindow;
if (ownerWindow is null)
return;
var pageName = NormalizePageName(pageAxaml);
if (string.IsNullOrWhiteSpace(pageName))
return;
var targetType = ResolvePageType(hostWindow, pageName);
if (targetType is null)
return;
var instance = CreateInstance(targetType, ctorArgs);
if (instance is null)
return;
if (fNewWindow)
{
_ = ShowModalLikeMessageBoxAsync(ownerWindow, instance);
return;
}
_ = Dispatcher.UIThread.InvokeAsync(() =>
ReplaceFromCurrent(current, instance, hostWindow));
}
catch (Exception ex)
{
Console.WriteLine($"[NavigationService.Navigate] 导航主流程异常: {ex}");
}
}
private static object? CreateInstance(Type targetType, object?[] ctorArgs)
{
try
{
if (ctorArgs is { Length: > 0 })
return Activator.CreateInstance(targetType, ctorArgs);
return Activator.CreateInstance(targetType);
}
catch (Exception ex)
{
Console.WriteLine(
$"[NavigationService.CreateInstance] 创建实例失败: {targetType.FullName}, args={ctorArgs?.Length ?? 0}, ex={ex}");
return null;
}
}
private static Window? ResolveHostWindow(object current)
{
try
{
if (current is Window w)
return w;
if (current is Control c)
return TopLevel.GetTopLevel(c) as Window;
if (TryGetHost(current, out var host) && host is Window hw)
return hw;
if (TryGetParent(current, out var parent))
return ResolveHostWindow(parent);
}
catch (Exception ex)
{
Console.WriteLine($"[ResolveHostWindow] 推导 HostWindow 失败: {ex}");
}
return null;
}
private static Window? ResolveOwnerWindowForModal(object current)
{
try
{
var main = GetMainWindow();
if (current is Window w)
{
if (w.IsVisible)
return w;
if (main is not null && main.IsVisible)
return main;
return w;
}
if (current is Control c)
{
var top = TopLevel.GetTopLevel(c) as Window;
if (top is not null && top.IsVisible)
return top;
if (main is not null && main.IsVisible)
return main;
return top ?? main;
}
if (TryGetHost(current, out var host) && host is Window hw)
return hw.IsVisible ? hw : (main?.IsVisible == true ? main : hw);
if (TryGetParent(current, out var parent))
return ResolveOwnerWindowForModal(parent);
return main;
}
catch (Exception ex)
{
Console.WriteLine($"[ResolveOwnerWindowForModal] 推导 OwnerWindow 失败: {ex}");
return null;
}
}
private static Window? GetMainWindow()
{
try
{
if (Application.Current?.ApplicationLifetime is IClassicDesktopStyleApplicationLifetime desktop)
return desktop.MainWindow;
}
catch (Exception ex)
{
Console.WriteLine($"[GetMainWindow] 获取 MainWindow 失败: {ex}");
}
return null;
}
private static string NormalizePageName(string pageAxaml)
{
try
{
var name = pageAxaml?.Trim() ?? string.Empty;
if (name.Length == 0)
return string.Empty;
name = Path.GetFileName(name);
if (name.EndsWith(".axaml", true, CultureInfo.InvariantCulture))
name = Path.GetFileNameWithoutExtension(name);
return name;
}
catch (Exception ex)
{
Console.WriteLine($"[NormalizePageName] 解析页面名失败: {pageAxaml}, ex={ex}");
return string.Empty;
}
}
private static Type? ResolvePageType(Window hostWindow, string pageName)
{
try
{
var t = ResolveType(hostWindow.GetType().Assembly, pageName);
if (t is not null)
return t;
t = ResolveType(Assembly.GetEntryAssembly(), pageName);
if (t is not null)
return t;
t = ResolveType(typeof(NavigationService).Assembly, pageName);
if (t is not null)
return t;
return ResolvePageTypeAllAssemblies(pageName);
}
catch (Exception ex)
{
Console.WriteLine($"[ResolvePageType] 查找页面类型失败: {pageName}, ex={ex}");
return null;
}
}
private static Type? ResolvePageTypeAllAssemblies(string pageName)
{
try
{
foreach (var asm in AppDomain.CurrentDomain.GetAssemblies())
{
// 跳过动态程序集/反射可能抛异常的情况由 ResolveType 内部处理
var t = ResolveType(asm, pageName);
if (t is not null)
return t;
}
}
catch (Exception ex)
{
Console.WriteLine($"[ResolvePageTypeAllAssemblies] 扫描所有程序集失败: page={pageName}, ex={ex}");
}
return null;
}
private static Type? ResolveType(Assembly? assembly, string pageName)
{
if (assembly is null)
return null;
return cache.GetOrAdd((assembly, pageName), key =>
{
try
{
return key.assembly
.GetTypes()
.FirstOrDefault(t =>
string.Equals(t.Name, key.pageName, StringComparison.OrdinalIgnoreCase));
}
catch (Exception ex)
{
Console.WriteLine(
$"[ResolveType] 扫描程序集失败: {assembly.FullName}, page={pageName}, ex={ex}");
return null;
}
});
}
private static async Task ShowModalLikeMessageBoxAsync(Window owner, object instance)
{
try
{
var realOwner = ResolveBestOwnerForModal(owner);
if (realOwner is null)
return;
if (!realOwner.IsVisible)
{
var ok = await WaitForOwnerVisibleWithTimeout(realOwner, 2000);
if (!ok || !realOwner.IsVisible)
{
var main = GetMainWindow();
if (main is not null && main.IsVisible)
realOwner = main;
else
{
await Dispatcher.UIThread.InvokeAsync(
() => ShowNonModalFallback(instance));
return;
}
}
}
await Dispatcher.UIThread.InvokeAsync(
() => ShowDialogCore(realOwner, instance));
}
catch (Exception ex)
{
Console.WriteLine($"[ShowModalLikeMessageBoxAsync] 显示模态窗口失败: {ex}");
}
}
private static Window? ResolveBestOwnerForModal(Window owner)
{
try
{
if (owner.IsVisible)
return owner;
var main = GetMainWindow();
if (main is not null && main.IsVisible)
return main;
return owner;
}
catch (Exception ex)
{
Console.WriteLine($"[ResolveBestOwnerForModal] 选择 Owner 失败: {ex}");
return null;
}
}
private static void ShowDialogCore(Window owner, object instance)
{
try
{
if (!owner.IsVisible)
return;
if (instance is Window window)
{
window.Icon = owner.Icon;
window.WindowStartupLocation = WindowStartupLocation.CenterOwner;
_ = window.ShowDialog(owner);
return;
}
var content = EnsureControl(instance);
if (content is null)
return;
var hostWindow = new Window
{
Icon = owner.Icon,
WindowStartupLocation = WindowStartupLocation.CenterOwner,
Content = content
};
_ = hostWindow.ShowDialog(owner);
}
catch (Exception ex)
{
Console.WriteLine($"[ShowDialogCore] ShowDialog 执行失败: {ex}");
}
}
private static void ShowNonModalFallback(object instance)
{
try
{
if (instance is Window w)
{
w.Show();
return;
}
var content = EnsureControl(instance);
if (content is null)
return;
new Window { Content = content }.Show();
}
catch (Exception ex)
{
Console.WriteLine($"[ShowNonModalFallback] 非模态显示失败: {ex}");
}
}
private static async Task<bool> WaitForOwnerVisibleWithTimeout(Window owner, int timeoutMs)
{
try
{
if (owner.IsVisible)
return true;
var tcs = new TaskCompletionSource<bool>();
void OnOpened(object? s, EventArgs e)
{
owner.Opened -= OnOpened;
tcs.TrySetResult(true);
}
owner.Opened += OnOpened;
var delay = Task.Delay(timeoutMs);
var done = await Task.WhenAny(tcs.Task, delay);
return done == tcs.Task && owner.IsVisible;
}
catch (Exception ex)
{
Console.WriteLine($"[WaitForOwnerVisibleWithTimeout] 等待 Owner 可见失败: {ex}");
return false;
}
}
private static Control? EnsureControl(object instance)
{
try
{
if (instance is Window w)
return w.Content as Control;
if (instance is Control c)
return c;
}
catch (Exception ex)
{
Console.WriteLine($"[EnsureControl] 提取 Control 失败: {ex}");
}
return null;
}
private static void ReplaceFromCurrent(object current, object instance, Window fallbackHostWindow)
{
try
{
var content = EnsureControl(instance);
if (content is null)
return;
if (TryGetHost(current, out var cachedHost) &&
TryReplaceOnHost(cachedHost, content))
{
CacheRelations(current, instance, content, cachedHost);
return;
}
object? host = current switch
{
Window w => w,
ContentControl cc => cc,
Control c => FindNearestContentHost(c),
_ => null
};
if (host is null && TryGetParent(current, out var parent))
{
if (TryGetHost(parent, out var parentHost))
host = parentHost;
else if (parent is Control pc)
host = FindNearestContentHost(pc);
else if (parent is Window pw)
host = pw;
}
host ??= fallbackHostWindow;
if (TryReplaceOnHost(host, content))
CacheRelations(current, instance, content, host);
}
catch (Exception ex)
{
Console.WriteLine($"[ReplaceFromCurrent] 替换 Content 失败: {ex}");
}
}
private static void CacheRelations(object current, object instance, Control content, object host)
{
CacheHost(current, host);
CacheHost(instance, host);
CacheHost(content, host);
CacheParent(instance, current);
CacheParent(content, current);
}
private static bool TryReplaceOnHost(object host, Control content)
{
try
{
if (host is Window w)
{
w.Content = content;
return true;
}
if (host is ContentControl cc)
{
cc.Content = content;
return true;
}
}
catch (Exception ex)
{
Console.WriteLine($"[TryReplaceOnHost] 写入 Content 失败: {ex}");
}
return false;
}
private static object? FindNearestContentHost(Control start)
{
try
{
Control? cur = start;
while (cur != null)
{
if (cur is ContentControl or Window)
return cur;
cur = cur.Parent as Control;
}
if (TryGetParent(start, out var parent))
{
if (parent is Control pc)
return FindNearestContentHost(pc);
if (parent is Window pw)
return pw;
}
return TopLevel.GetTopLevel(start) as Window;
}
catch (Exception ex)
{
Console.WriteLine($"[FindNearestContentHost] 查找 Host 失败: {ex}");
return null;
}
}
private static Relation GetOrCreateRelation(object key)
{
return relationMap.GetValue(key, _ => new Relation());
}
private static bool TryGetHost(object key, out object host)
{
if (relationMap.TryGetValue(key, out var relation) &&
relation.Host is not null)
{
host = relation.Host;
return true;
}
host = null!;
return false;
}
private static void CacheHost(object key, object host)
{
GetOrCreateRelation(key).Host = host;
}
private static void CacheParent(object child, object parent)
{
GetOrCreateRelation(child).Parent = parent;
}
private static bool TryGetParent(object child, out object parent)
{
if (relationMap.TryGetValue(child, out var relation) &&
relation.Parent is not null)
{
parent = relation.Parent;
return true;
}
parent = null!;
return false;
}
}
内嵌WebView的HyperLink类
public class Hyperlink : InlineUIContainer
{
public sealed class WebBrowserPage : UserControl
{
public WebBrowserPage(string url)
{
var webView = new WebView()
{
HorizontalAlignment = HorizontalAlignment.Stretch,
VerticalAlignment = VerticalAlignment.Stretch
};
Content = webView;
if (!string.IsNullOrWhiteSpace(url) &&
Uri.TryCreate(url, UriKind.Absolute, out var uri))
{
webView.Navigate(uri);
}
}
~WebBrowserPage()
{
(this.Content as WebView)!.Dispose();
}
}
private readonly TextBlock textBlock;
private ContextMenu? contextMenu;
public event EventHandler<PointerPressedEventArgs>? Click;
public static readonly StyledProperty<string?> TextProperty =
AvaloniaProperty.Register<Hyperlink, string?>(nameof(Text));
public string? Text
{
get => GetValue(TextProperty);
set => SetValue(TextProperty, value);
}
public static readonly StyledProperty<string?> ContentTextProperty =
AvaloniaProperty.Register<Hyperlink, string?>(nameof(ContentText));
[Content]
public string? ContentText
{
get => GetValue(ContentTextProperty);
set => SetValue(ContentTextProperty, value);
}
public static readonly StyledProperty<string?> NavigateUriProperty =
AvaloniaProperty.Register<Hyperlink, string?>(nameof(NavigateUri));
public string? NavigateUri
{
get => GetValue(NavigateUriProperty);
set => SetValue(NavigateUriProperty, value);
}
public static readonly StyledProperty<bool> OpenInNewWindowProperty =
AvaloniaProperty.Register<Hyperlink, bool>(nameof(OpenInNewWindow), true);
public bool OpenInNewWindow
{
get => GetValue(OpenInNewWindowProperty);
set => SetValue(OpenInNewWindowProperty, value);
}
public static readonly StyledProperty<string> WebBrowserPageNameProperty =
AvaloniaProperty.Register<Hyperlink, string>(
nameof(WebBrowserPageName),
nameof(WebBrowserPage));
public string WebBrowserPageName
{
get => GetValue(WebBrowserPageNameProperty);
set => SetValue(WebBrowserPageNameProperty, value);
}
public Hyperlink()
{
textBlock = new TextBlock
{
TextDecorations =
[
new TextDecoration { Location = TextDecorationLocation.Underline }
],
Foreground = Brushes.Blue,
Cursor = new Cursor(StandardCursorType.Hand),
Margin = new Thickness(0, 0, 0, -2),
Transitions =
[
new DoubleTransition
{
Property = Visual.OpacityProperty,
Duration = TimeSpan.FromSeconds(0.5),
Easing = new SineEaseInOut()
}
]
};
this.GetObservable(TextProperty).Subscribe(_ => UpdateText());
this.GetObservable(ContentTextProperty).Subscribe(_ => UpdateText());
textBlock[!!Visual.OpacityProperty] = this[!!Visual.OpacityProperty];
textBlock.PointerPressed += OnPointerPressed;
textBlock.PointerReleased += OnPointerReleased;
Child = textBlock;
UpdateText();
}
private void UpdateText()
{
if (!string.IsNullOrWhiteSpace(Text))
{
textBlock.Text = Text;
return;
}
if (!string.IsNullOrWhiteSpace(ContentText))
{
textBlock.Text = ContentText;
return;
}
textBlock.Text = string.Empty;
}
private void OnPointerPressed(object? sender, PointerPressedEventArgs e)
{
Click?.Invoke(this, e);
if (!e.GetCurrentPoint(textBlock).Properties.IsRightButtonPressed)
return;
var target = NavigateUri;
if (string.IsNullOrWhiteSpace(target))
return;
if (!TryNormalizeHttpUrl(target, out _))
return;
ShowContextMenu();
e.Handled = true;
}
private void OnPointerReleased(object? sender, PointerReleasedEventArgs e)
{
if (e.InitialPressMouseButton != MouseButton.Left)
return;
HandleNavigateFromClick(e.KeyModifiers);
e.Handled = true;
}
private void HandleNavigateFromClick(KeyModifiers modifiers)
{
var target = NavigateUri;
if (string.IsNullOrWhiteSpace(target))
return;
var ctrl = (modifiers & KeyModifiers.Control) != 0;
if (TryNormalizeHttpUrl(target, out var httpUrl))
{
if (ctrl)
{
TryOpenExternalUrl(httpUrl);
return;
}
if (OpenInNewWindow)
{
TryOpenExternalUrl(httpUrl);
return;
}
NavigationService.Navigate(
textBlock,
WebBrowserPageName,
fNewWindow: false,
ctorArgs: [httpUrl]);
return;
}
var openNew = ctrl || OpenInNewWindow;
NavigationService.Navigate(textBlock, target, openNew);
}
private void ShowContextMenu()
{
var target = NavigateUri;
if (string.IsNullOrWhiteSpace(target))
return;
if (!TryNormalizeHttpUrl(target, out _))
return;
if (contextMenu == null)
contextMenu = BuildContextMenu();
contextMenu.PlacementTarget = textBlock;
contextMenu.Open(textBlock);
}
private ContextMenu BuildContextMenu()
{
var copyItem = new MenuItem
{
Header = MenuText("Copy link")
};
copyItem.Click += async (_, __) =>
{
var target = NavigateUri;
if (string.IsNullOrWhiteSpace(target))
return;
var top = TopLevel.GetTopLevel(textBlock);
var cb = top?.Clipboard;
if (cb == null)
return;
await cb.SetTextAsync(target);
};
var openExternalItem = new MenuItem
{
Header = MenuText("Open in new window")
};
openExternalItem.Click += (_, __) =>
{
var target = NavigateUri;
if (TryNormalizeHttpUrl(target ?? "", out var httpUrl))
TryOpenExternalUrl(httpUrl);
};
var openEmbeddedItem = new MenuItem
{
Header = MenuText("Open embedded")
};
openEmbeddedItem.Click += (_, __) =>
{
var target = NavigateUri;
if (TryNormalizeHttpUrl(target ?? "", out var httpUrl))
{
NavigationService.Navigate(
textBlock,
WebBrowserPageName,
fNewWindow: false,
ctorArgs: [httpUrl]);
}
};
return new ContextMenu
{
Items =
{
copyItem,
new Separator(),
openExternalItem,
openEmbeddedItem
}
};
}
private static TextBlock MenuText(string text)
{
return new TextBlock
{
Text = text,
TextDecorations = null
};
}
private static bool TryNormalizeHttpUrl(string target, out string normalized)
{
normalized = "";
if (!Uri.TryCreate(target, UriKind.Absolute, out var uri))
return false;
if (uri.Scheme != Uri.UriSchemeHttp && uri.Scheme != Uri.UriSchemeHttps)
return false;
normalized = uri.ToString();
return true;
}
private static void TryOpenExternalUrl(string target)
{
try
{
Process.Start(new ProcessStartInfo
{
FileName = target,
UseShellExecute = true
});
}
catch
{
}
}
}
浙公网安备 33010602011771号