我们在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();
即可。