Jx.Cms开发笔记(六)-重写Compiler

我们在Jx.Cms开发笔记(三)-Views主题动态切换中说了如何切换主题。但是这里有一个问题,就是主题切换时,会报错

theme.png

这是由于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的,所以我们只能把这部分内容全部重写。

我们创建自己的MyViewCompilerProviderMyViewCompiler。由于.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源码里的构造函数拆分了,拆出了一个PublicModify方法。

然后我们需要用自己的MyViewCompilerProvider替换自带的,所以我们需要在Startup.csConfigureServices方法中添加services.Replace<IViewCompilerProvider, MyViewCompilerProvider>();

最后我们只需要在需要重新获取所有Views的地方调用viewCompilerProvider?.Modify();即可。

页面下部广告