Jx.Cms開發筆記(六)-重寫Compiler
我們在Jx.Cms開發筆記(三)-Views主題動態切換中說了如何切換主題。但是這裡有一個問題,就是主題切換時,會報錯
這是由於asp.net core在處理Views
的資訊的時候是在建構函式中處理的,沒有任何方法可以重新整理這個處理結果。
這裡放最新版的DefaultViewCompiler
程式碼,在Jx.Cms編寫的時候程式碼有少許區別,但是基本邏輯是一樣的。
public DefaultViewCompiler( ApplicationPartManager applicationPartManager, ILogger<DefaultViewCompiler> logger) { _applicationPartManager = applicationPartManager; _logger = logger; _normalizedPathCache = new ConcurrentDictionary<string, string>(StringComparer.Ordinal); EnsureCompiledViews(logger); } [MemberNotNull(nameof(_compiledViews))] private void EnsureCompiledViews(ILogger logger) { if (_compiledViews is not null) { return; } var viewsFeature = new ViewsFeature(); _applicationPartManager.PopulateFeature(viewsFeature); // We need to validate that the all compiled views are unique by path (case-insensitive). // We do this because there's no good way to canonicalize paths on windows, and it will create // problems when deploying to linux. Rather than deal with these issues, we just don't support // views that differ only by case. var compiledViews = new Dictionary<string, Task<CompiledViewDescriptor>>( viewsFeature.ViewDescriptors.Count, StringComparer.OrdinalIgnoreCase); foreach (var compiledView in viewsFeature.ViewDescriptors) { logger.ViewCompilerLocatedCompiledView(compiledView.RelativePath); if (!compiledViews.ContainsKey(compiledView.RelativePath)) { // View ordering has precedence semantics, a view with a higher precedence was not // already added to the list. compiledViews.TryAdd(compiledView.RelativePath, Task.FromResult(compiledView)); } } if (compiledViews.Count == 0) { logger.ViewCompilerNoCompiledViewsFound(); } // Safe races should be ok. We would end up logging multiple times // if this is invoked concurrently, but since this is primarily a dev-scenario, we don't think // this will happen often. We could always re-consider the logging if we get feedback. _compiledViews = compiledViews; }
所以程式只能獲取到第一次的_compiledViews
,切換後的Views
由於沒有放在_compiledViews
中,所以無法被找到,就出現了第一圖的那種錯誤。
這裡的解決方法很簡單,我們只需要重寫一個自己的ViewCompiler
就可以了,由於官方原始碼全部都是internal
的,所以我們只能把這部分內容全部重寫。
我們建立自己的MyViewCompilerProvider
和MyViewCompiler
。由於.Net6在這裡有部分修改,我們的程式是在.Net5時編寫的,所以這裡我們的原始碼是.Net5修改的,與目前最新的程式碼有些許差距,但是不影響正常使用.
MyViewCompiler:
public class MyViewCompiler : IViewCompiler { private readonly Dictionary<string, Task<CompiledViewDescriptor>> _compiledViews; private readonly ConcurrentDictionary<string, string> _normalizedPathCache; private readonly ILogger _logger; public MyViewCompiler( IList<CompiledViewDescriptor> compiledViews, ILogger logger) { if (compiledViews == null) { throw new ArgumentNullException(nameof(compiledViews)); } if (logger == null) { throw new ArgumentNullException(nameof(logger)); } _logger = logger; _normalizedPathCache = new ConcurrentDictionary<string, string>(StringComparer.Ordinal); // We need to validate that the all of the precompiled views are unique by path (case-insensitive). // We do this because there's no good way to canonicalize paths on windows, and it will create // problems when deploying to linux. Rather than deal with these issues, we just don't support // views that differ only by case. _compiledViews = new Dictionary<string, Task<CompiledViewDescriptor>>( compiledViews.Count, StringComparer.OrdinalIgnoreCase); foreach (var compiledView in compiledViews) { if (!_compiledViews.ContainsKey(compiledView.RelativePath)) { // View ordering has precedence semantics, a view with a higher precedence was not // already added to the list. _compiledViews.Add(compiledView.RelativePath, Task.FromResult(compiledView)); } } if (_compiledViews.Count == 0) { } } /// <inheritdoc /> public Task<CompiledViewDescriptor> CompileAsync(string relativePath) { if (relativePath == null) { throw new ArgumentNullException(nameof(relativePath)); } // Attempt to lookup the cache entry using the passed in path. This will succeed if the path is already // normalized and a cache entry exists. if (_compiledViews.TryGetValue(relativePath, out var cachedResult)) { return cachedResult; } var normalizedPath = GetNormalizedPath(relativePath); if (_compiledViews.TryGetValue(normalizedPath, out cachedResult)) { return cachedResult; } // Entry does not exist. Attempt to create one. return Task.FromResult(new CompiledViewDescriptor { RelativePath = normalizedPath, ExpirationTokens = Array.Empty<IChangeToken>(), }); } private string GetNormalizedPath(string relativePath) { Debug.Assert(relativePath != null); if (relativePath.Length == 0) { return relativePath; } if (!_normalizedPathCache.TryGetValue(relativePath, out var normalizedPath)) { normalizedPath = NormalizePath(relativePath); _normalizedPathCache[relativePath] = normalizedPath; } return normalizedPath; } public static string NormalizePath(string path) { var addLeadingSlash = path[0] != '\\' && path[0] != '/'; var transformSlashes = path.IndexOf('\\') != -1; if (!addLeadingSlash && !transformSlashes) { return path; } var length = path.Length; if (addLeadingSlash) { length++; } return string.Create(length, (path, addLeadingSlash), (span, tuple) => { var (pathValue, addLeadingSlashValue) = tuple; var spanIndex = 0; if (addLeadingSlashValue) { span[spanIndex++] = '/'; } foreach (var ch in pathValue) { span[spanIndex++] = ch == '\\' ? '/' : ch; } }); } }
這個類完全複製了.Net5的原始碼,只是刪除了部分編譯不過去的日誌內容。
MyViewCompilerProvider:
public class MyViewCompilerProvider : IViewCompilerProvider
{
private MyViewCompiler _compiler;
private readonly ApplicationPartManager _applicationPartManager;
private readonly ILoggerFactory _loggerFactory;
public MyViewCompilerProvider(
ApplicationPartManager applicationPartManager,
ILoggerFactory loggerFactory)
{
_applicationPartManager = applicationPartManager;
_loggerFactory = loggerFactory;
Modify();
}
public void Modify()
{
var feature = new ViewsFeature();
_applicationPartManager.PopulateFeature(feature);
_compiler = new MyViewCompiler(feature.ViewDescriptors, _loggerFactory.CreateLogger<MyViewCompiler>());
}
public IViewCompiler GetCompiler() => _compiler;
}
這個類我們只是把.Net5原始碼裡的建構函式拆分了,拆出了一個Public
的Modify
方法。
然後我們需要用自己的MyViewCompilerProvider
替換自帶的,所以我們需要在Startup.cs
的ConfigureServices
方法中新增services.Replace<IViewCompilerProvider, MyViewCompilerProvider>();
最後我們只需要在需要重新獲取所有Views
的地方呼叫viewCompilerProvider?.Modify();
即可。