在 .NET Framework 時代里面,有一組有趣的概念,那就是 SDK 和 Runtime 這兩個概念,開發模式十分有趣,在開發者設備上,可以指定 .NET Framework 的 SDK 版本,例如指定 .NET Framework 4.5 版本,開發完成之后,分發給到用戶,用戶的電腦上所安裝的 .NET Framework 基本都是 Runtime 版本,應用程式要求運行的 Runtime 版本一定要大于等于 SDK 的指定版本號
這就有一個非常有趣的問題了,我開發環境使用的 SDK 是低版本,例如 .NET Framework 4.5 版本,但用戶的電腦上所安裝的 .NET Framework 的 Runtime 版本是高版本,例如是 .NET Framework 4.7 版本,中間距離過了幾年的版本,那行為如何保證相同?如果行為不能保證相同,那將會出現行為差異,在我的開發設備上跑得好好的,在一些用戶電腦上,將會因為運行時基礎庫的版本差異從而變更行為,應用程式就不能符合預期跑起來了,事實上,咱沒有碰到過這個問題,這是因為在 .NET Framework 層做了很多兼容處理邏輯,其中就包括本文要和大家聊的 WPF 框架的兼容行為
本文所聊的僅僅只是 .NET Framework 的老故事,本文對 .NET Core 3.0 及以上版本基本無效,因為其開發模式變更了
在 .NET Framework 的開發模式里面,開發者采用的是 .NET Framework SDK 進行開發,可以指定版本號,指定版本號后,生成的應用程式要求在運行的設備上,一定要安裝大于或等于指定版本的運行時才可以
在 .NET Framework 下,無法讓 .NET Framework 的運行時跟隨應用進行分發,應用只能采用系統里安裝的版本,存在的問題就是應用在開發階段是無法預知實際運行在用戶端,用戶的設備上所安裝的 .NET Framework 版本的
如果對于相同的一套代碼,在不同的 .NET Framework 版本下,有行為上的差異,那這是一個非常可怕的事情,甚至開發端啥都做不了,想想如以下的例子,假如我是 .NET Framework 版本的 WPF 框架的開發者,我寫了一個叫 Foo 的方法,這個方法我是在 .NET Framework 4.5 定義的,用于支持某個關鍵邏輯,但是我在 .NET Framework 4.6 版本,我覺得這個方法應該拋出例外,于是就讓這個 Foo 方法被呼叫時一定拋出例外
這個時候,預計開發者將會寄刀片給我,因為開發者將會發現他的應用能在一些用戶的設備上,符合預期作業,在一些用戶的設備上,一定會炸掉,其原因就是我在高版本上變更了行為,根本原因就是開發者無法獨立分發 .NET Framework 導致應用程式的運行受實際的環境影響
當然了,這個問題在 .NET Core 或 .NET 下已經被徹底解決,那就是允許開發者自己獨立發布,不依賴系統的 .NET 環境,如此的開發模式,既可以降低基礎運行時框架開發者的壓力,也能讓上層應用業務端的開發者能更好的控制應用運行,減少奇怪的環境問題
作為考古的博客,繼續回到主題
在實際的開發里面,幾乎沒有遇到上面說的由于 .NET Framework 的 Runtime 版本不同,而導致的行為不同從而導致的問題
這是因為在 WPF 里面做了很多的兼容的處理邏輯,例如大家都知道的,在 .NET Framework 4.0 到 .NET Framework 4.5 是存在極大的變動的,在 WPF 里面,為了撰寫代碼方便,就添加了一個叫 FrameworkCompatibilityPreferences 的型別,里面有一個屬性如下
public static class FrameworkCompatibilityPreferences
{
static FrameworkCompatibilityPreferences()
{
#if NETFX && !NETCOREAPP
_targetsDesktop_V4_0 = BinaryCompatibility.AppWasBuiltForFramework == TargetFrameworkId.NetFramework
&& !BinaryCompatibility.TargetsAtLeast_Desktop_V4_5;
#elif NETCOREAPP
// When building for NETCOREAPP, set this to false
// to indicate that quirks should be treated as if they are running on
// .NET 4.5+
_targetsDesktop_V4_0 = false;
#else
_targetsDesktop_V4_0 = false;
#endif
}
// CLR's BinaryCompatibility class doesn't expose a convenient way to determine
// if the app targets 4.0 exactly. We use that a lot, so encapsulate it here
static bool _targetsDesktop_V4_0;
internal static bool TargetsDesktop_V4_0
{
get { return _targetsDesktop_V4_0; }
}
}
在各處代碼里面對此屬性的判斷,從而加上兼容的邏輯,例如系結的代碼
public class Binding : BindingBase
{
public PropertyPath Path
{
get { return _ppath; }
set
{
_ppath = value;
if (_ppath != null && _ppath.StartsWithStaticProperty)
{
if (_sourceInUse == SourceProperties.None || _sourceInUse == SourceProperties.StaticSource
// 在 .NET Framework 4.5 和以上的版本更改了行為,需要加上兼容邏輯
|| FrameworkCompatibilityPreferences.TargetsDesktop_V4_0)
{
// net 4.5 breaks static bindings - this is for compat
SourceReference = StaticSourceRef;
}
else
throw new InvalidOperationException(SR.Get(SRID.BindingConflict, SourceProperties.StaticSource, _sourceInUse));
}
}
}
}
毫無疑問,通過一個屬性的方式確實簡單明了,缺點也很明顯,代碼里面將會充斥版本判斷邏輯,這個方式是走不遠的,這個也算是兼容的包袱
作為一群對代碼有追求的開發者,肯定是要做一些設計,于是大家可以看到 WPF 開源倉庫里面,實際上現在主流的還有一層兼容邏輯,那就是功能開關
依托 System.AppContext 提供的組件之間建立松耦合的協定,從而將代碼里面原本對版本強依賴的邏輯變更為對功能的支持
這是一個很好的思路,讓功能的開關與具體的 .NET Framework 版本關聯,讓實際的功能邏輯和功能開關關聯,如此即可進行邏輯控制且減少在代碼里面判斷版本的邏輯,也不會在后續隨著版本越來越多,維護越來越難
另外,功能開關本身和具體的 .NET Framework 版本只是一個弱相關性而已,僅僅只是在應用程式運行的時候,在應用程式配置里面宣告這個應用程式所期望運行的 .NET Framework 版本,再根據此宣告使用的版本,決定開關的開啟和關閉
例如在 AppContextDefaultValues 里的代碼
internal static partial class AppContextDefaultValues
{
static partial void PopulateDefaultValuesPartial(string platformIdentifier, string profile, int targetFrameworkVersion)
{
switch (platformIdentifier)
{
case ".NETFramework":
{
if (targetFrameworkVersion <= 40601)
{
LocalAppContext.DefineSwitchDefault(CoreAppContextSwitches.DoNotScaleForDpiChangesSwitchName, true);
}
if (targetFrameworkVersion <= 40602)
{
LocalAppContext.DefineSwitchDefault(CoreAppContextSwitches.OverrideExceptionWithNullReferenceExceptionName, true);
}
if (targetFrameworkVersion <= 40702)
{
LocalAppContext.DefineSwitchDefault(CoreAppContextSwitches.DoNotUsePresentationDpiCapabilityTier2OrGreaterSwitchName, true);
}
break;
}
case ".NETCoreApp":
{
InitializeNetFxSwitchDefaultsForNetCoreRuntime();
}
break;
}
}
private static void InitializeNetFxSwitchDefaultsForNetCoreRuntime()
{
LocalAppContext.DefineSwitchDefault(CoreAppContextSwitches.DoNotScaleForDpiChangesSwitchName, false);
LocalAppContext.DefineSwitchDefault(CoreAppContextSwitches.OverrideExceptionWithNullReferenceExceptionName, false);
LocalAppContext.DefineSwitchDefault(CoreAppContextSwitches.DoNotUsePresentationDpiCapabilityTier2OrGreaterSwitchName, false);
LocalAppContext.DefineSwitchDefault(CoreAppContextSwitches.DisableStylusAndTouchSupportSwitchName, false);
LocalAppContext.DefineSwitchDefault(CoreAppContextSwitches.EnablePointerSupportSwitchName, false);
LocalAppContext.DefineSwitchDefault(CoreAppContextSwitches.DisableDiagnosticsSwitchName, false);
LocalAppContext.DefineSwitchDefault(CoreAppContextSwitches.AllowChangesDuringVisualTreeChangedSwitchName, false);
LocalAppContext.DefineSwitchDefault(CoreAppContextSwitches.DisableImplicitTouchKeyboardInvocationSwitchName, false);
LocalAppContext.DefineSwitchDefault(CoreAppContextSwitches.ShouldRenderEvenWhenNoDisplayDevicesAreAvailableSwitchName, false);
LocalAppContext.DefineSwitchDefault(CoreAppContextSwitches.ShouldNotRenderInNonInteractiveWindowStationSwitchName, false);
LocalAppContext.DefineSwitchDefault(CoreAppContextSwitches.DoNotUsePresentationDpiCapabilityTier3OrGreaterSwitchName, false);
LocalAppContext.DefineSwitchDefault(CoreAppContextSwitches.AllowExternalProcessToBlockAccessToTemporaryFilesSwitchName, false);
}
}
當然,功能開關的作用可不只是用來做兼容邏輯,還可以用來實作特殊的配置,從而打開某些功能或者禁用某些功能,更方便開發者進行定制,更多的功能開關請看 WPF Application Compatibility switches list
這就是 WPF 框架對 .NET Framework 運行時版本不同進行的適配邏輯,通過在代碼里面添加大量的判斷條件,從而讓開發者指定的運行版本和實際運行所采用的邏輯盡可能相同,這個兼容邏輯是 WPF 參考里面寫代碼實作的,也就是靠 WPF 框架的開發者保證的,這也就是本文開頭我用了 幾乎 而不是確定詞來說不會在這里踩坑
博客園博客只做備份,博客發布就不再更新,如果想看最新博客,請到 https://blog.lindexi.com/

本作品采用知識共享署名-非商業性使用-相同方式共享 4.0 國際許可協議進行許可,歡迎轉載、使用、重新發布,但務必保留文章署名[林德熙](http://blog.csdn.net/lindexi_gd)(包含鏈接:http://blog.csdn.net/lindexi_gd ),不得用于商業目的,基于本文修改后的作品務必以相同的許可發布,如有任何疑問,請與我[聯系](mailto:[email protected]),
轉載請註明出處,本文鏈接:https://www.uj5u.com/net/508726.html
標籤:WPF
上一篇:VisualStudio 禁用移動檔案到檔案夾自動修改命名空間功能
下一篇:如何撰寫一個函式來查找字串陣列中的最長公共前綴,說明:所有輸入只包含小寫字母a~z ,如果不存在公共前綴,回傳空字串
