feat(legacy): add shell-independent module page compatibility

This commit is contained in:
2026-08-28 11:56:48 +08:00
parent 8472004711
commit 1c59fda8b8
8 changed files with 387 additions and 21 deletions
+305
View File
@@ -88,6 +88,311 @@ namespace Lskj.Model
/// 主界面page界面
/// </summary>
public XtraTabControl PageControl;
private readonly object _modulePageCallbackSync = new object();
private readonly List<Action<string>> _modulePageSelectedCallbacks =
new List<Action<string>>();
/// <summary>
/// 顶层页面生命周期服务。旧 WinForms 主程序保持为空并继续使用
/// PageControlWPF 主程序注入自己的页面宿主实现。
/// </summary>
public IModulePageHost ModulePageHost;
public XtraTabPage CurrentModulePage
{
get
{
if (ModulePageHost != null)
return ModulePageHost.SelectedPage;
return PageControl == null ? null : PageControl.SelectedTabPage;
}
set
{
// Keep the original assignment-based API working for legacy
// modules. In the classic WinForms shell this forwards to
// the real PageControl; when WPF injects ModulePageHost it
// selects the page through that host instead of creating a
// second XtraTabControl.
SelectModulePage(value);
}
}
public IEnumerable<XtraTabPage> ModulePages
{
get
{
if (ModulePageHost != null)
return ModulePageHost.Pages;
if (PageControl == null)
return new XtraTabPage[0];
return PageControl.TabPages.Cast<XtraTabPage>();
}
}
/// <summary>
/// Indicates whether a top-level module page surface has been
/// initialized. The classic shell exposes its XtraTabControl while
/// WPF exposes IModulePageHost; callers can use this without reaching
/// into either visual implementation.
/// </summary>
public bool HasModulePageSurface
{
get { return ModulePageHost != null || PageControl != null; }
}
public void AddModulePage(XtraTabPage page)
{
if (page == null)
return;
if (ModulePageHost != null)
{
ModulePageHost.AddPage(page);
return;
}
if (PageControl != null && !PageControl.TabPages.Contains(page))
PageControl.TabPages.Add(page);
}
public bool RemoveModulePage(XtraTabPage page)
{
if (page == null)
return false;
if (ModulePageHost != null)
return ModulePageHost.RemovePage(page);
if (PageControl == null || !PageControl.TabPages.Contains(page))
return false;
PageControl.TabPages.Remove(page);
return true;
}
public bool SelectModulePage(XtraTabPage page)
{
if (page == null)
return false;
if (ModulePageHost != null)
return ModulePageHost.SelectPage(page);
if (PageControl == null || !PageControl.TabPages.Contains(page))
return false;
PageControl.SelectedTabPage = page;
return true;
}
public bool ContainsModulePage(XtraTabPage page)
{
if (page == null)
return false;
if (ModulePageHost != null)
return ModulePageHost.ContainsPage(page);
return PageControl != null && PageControl.TabPages.Contains(page);
}
/// <summary>
/// Returns the stable logical identifier used to bind a legacy page to
/// its WPF tab. The existing Tag value is retained whenever the old
/// module already assigned one; a generated value is written only for
/// an otherwise unidentified page.
/// </summary>
public string GetModulePageId(XtraTabPage page)
{
if (page == null)
return string.Empty;
string pageId = Convert.ToString(page.Tag);
if (string.IsNullOrWhiteSpace(pageId))
{
pageId = "legacy-module-" + Guid.NewGuid().ToString("N");
page.Tag = pageId;
}
return pageId;
}
/// <summary>
/// Gets the logical identifier of the selected top-level page without
/// exposing the shell's visual tab-control implementation.
/// </summary>
public string CurrentModulePageId
{
get { return GetModulePageId(CurrentModulePage); }
}
/// <summary>
/// Enumerates top-level page identifiers for modules that only need to
/// compare or select pages and should not reach PageControl.TabPages.
/// </summary>
public IEnumerable<string> GetModulePageIds()
{
return ModulePages.Select(GetModulePageId).ToArray();
}
/// <summary>
/// Selects a top-level page by its logical identifier. Both the WPF
/// host and the classic WinForms PageControl use the same fallback
/// path as the original XtraTabPage-based API.
/// </summary>
public bool SelectModulePage(string pageId)
{
XtraTabPage page = FindModulePage(pageId);
return page != null && SelectModulePage(page);
}
/// <summary>
/// Removes a top-level page by its logical identifier.
/// </summary>
public bool RemoveModulePage(string pageId)
{
XtraTabPage page = FindModulePage(pageId);
return page != null && RemoveModulePage(page);
}
/// <summary>
/// Indicates whether the supplied logical identifier belongs to a
/// top-level module page. This replaces checks based on
/// page.Parent as XtraTabControl in migrated module code.
/// </summary>
public bool IsTopLevelModulePage(string pageId)
{
return FindModulePage(pageId) != null;
}
/// <summary>
/// Registers a logical page-selection callback. This is intentionally
/// a page-service callback rather than a subscription to a concrete
/// WinForms or WPF tab-control event. Dispose the returned token when
/// the module closes so the callback cannot retain the module.
/// </summary>
public IDisposable RegisterModulePageSelected(Action<string> callback)
{
if (callback == null)
throw new ArgumentNullException("callback");
lock (_modulePageCallbackSync)
{
_modulePageSelectedCallbacks.Add(callback);
}
return new ModulePageCallbackRegistration(this, callback);
}
/// <summary>
/// Registers a callback for one logical module page. This overload is
/// the source-compatible replacement for code that used to subscribe
/// to the top-level PageControl.SelectedPageChanged event and then
/// filter by the selected page. The callback receives no WinForms
/// event arguments, so the same module code works when the WPF shell
/// owns the visible tab while the direct WinForms shell still uses its
/// original XtraTabControl.
/// </summary>
public IDisposable RegisterModulePageSelected(
string modulePageId,
Action callback)
{
if (callback == null)
throw new ArgumentNullException("callback");
string expectedPageId = (modulePageId ?? string.Empty).Trim();
if (string.IsNullOrWhiteSpace(expectedPageId))
throw new ArgumentException(
"模块页面标识不能为空。",
"modulePageId");
return RegisterModulePageSelected(
selectedPageId =>
{
if (string.Equals(
selectedPageId,
expectedPageId,
StringComparison.Ordinal))
{
callback();
}
});
}
/// <summary>
/// Notifies logical page-selection subscribers. The WPF page host
/// calls this after its own selection state is updated; the classic
/// MainPanelControlEx calls it from its existing SelectedPageChanged
/// handler. No visual control event is introduced in the WPF path.
/// </summary>
public void NotifyModulePageSelected(XtraTabPage page)
{
if (page == null)
return;
string pageId = GetModulePageId(page);
Action<string>[] callbacks;
lock (_modulePageCallbackSync)
{
callbacks = _modulePageSelectedCallbacks.ToArray();
}
foreach (Action<string> callback in callbacks)
{
try
{
callback(pageId);
}
catch (Exception exception)
{
System.Diagnostics.Trace.WriteLine(
"旧模块页面回调执行失败:" + exception);
}
}
}
private XtraTabPage FindModulePage(string pageId)
{
if (string.IsNullOrWhiteSpace(pageId))
return null;
foreach (XtraTabPage page in ModulePages)
{
if (string.Equals(
GetModulePageId(page),
pageId,
StringComparison.Ordinal))
{
return page;
}
}
return null;
}
private void UnregisterModulePageSelected(Action<string> callback)
{
if (callback == null)
return;
lock (_modulePageCallbackSync)
{
_modulePageSelectedCallbacks.Remove(callback);
}
}
private sealed class ModulePageCallbackRegistration : IDisposable
{
private ERPInfo _owner;
private readonly Action<string> _callback;
public ModulePageCallbackRegistration(
ERPInfo owner,
Action<string> callback)
{
_owner = owner;
_callback = callback;
}
public void Dispose()
{
ERPInfo owner = _owner;
if (owner == null)
return;
_owner = null;
owner.UnregisterModulePageSelected(_callback);
}
}
/// <summary>
/// 主界面page界面字典
/// </summary>
+2 -1
View File
@@ -35,6 +35,7 @@
<UseVSHostingProcess>false</UseVSHostingProcess>
</PropertyGroup>
<ItemGroup>
<Compile Include="ModulePageHost.cs" />
<Reference Include="DevExpress.Utils.v15.2, Version=15.2.4.0, Culture=neutral, PublicKeyToken=b88d1754d700e49a, processorArchitecture=MSIL">
<SpecificVersion>False</SpecificVersion>
<HintPath>..\..\Debug\DevExpress.Utils.v15.2.dll</HintPath>
@@ -111,4 +112,4 @@
<Target Name="AfterBuild">
</Target>
-->
</Project>
</Project>
+38
View File
@@ -0,0 +1,38 @@
using System;
using System.Collections.Generic;
using DevExpress.XtraTab;
namespace Lskj.Model
{
/// <summary>
/// Provides the top-level module page lifecycle without coupling module
/// code to a particular visual tab control. The WPF shell supplies an
/// implementation backed by DXTabItem instances; the classic WinForms
/// shell leaves this interface unset and ERPInfo falls back to its real
/// XtraTabControl/PageControl.
/// </summary>
public interface IModulePageHost
{
event EventHandler<ModulePageEventArgs> PageAdded;
event EventHandler<ModulePageEventArgs> PageRemoved;
event EventHandler<ModulePageEventArgs> PageSelected;
XtraTabPage SelectedPage { get; }
IEnumerable<XtraTabPage> Pages { get; }
void AddPage(XtraTabPage page);
bool RemovePage(XtraTabPage page);
bool SelectPage(XtraTabPage page);
bool ContainsPage(XtraTabPage page);
}
public sealed class ModulePageEventArgs : EventArgs
{
public ModulePageEventArgs(XtraTabPage page)
{
Page = page;
}
public XtraTabPage Page { get; private set; }
}
}