我可以在App_Code之外使用全局剃刀@helper吗?

时间:2012-05-19 12:18:47

标签: asp.net-mvc-3 razor helper app-code

标题中所述的问题很简单:有没有办法在'App_Code'之外使用剃刀助手?

示例(HtmlEx.cshtml文件):

@helper Script(string fileName, UrlHelper url)
{
<script src="@url.Content("~/Scripts/" + fileName)" type="text/javascript"></script> 
}

我问这个因为我在App_Code中没有任何其他内容;我想把我的项目结构有点不同。

感谢。

更新:我不想要任何其他类型的扩展程序。斯科特在这里谈到的只有纯粹的剃刀助手:http://weblogs.asp.net/scottgu/archive/2011/05/12/asp-net-mvc-3-and-the-helper-syntax-within-razor.aspx

4 个答案:

答案 0 :(得分:18)

  

标题中所述的问题很简单:有没有办法   在App_Code&#39;?

之外有剃刀助手

不,没有。

答案 1 :(得分:6)

永不言败......

方法一:(用于Web应用程序项目)

只需添加预构建事件即可将文件复制到App_Code文件夹中。

(但由于文件必须包含在项目中,您可以向App_Code目录添加一个具有相同名称的空文件,然后使用build事件来更新它。)

(注意,即使您最初将文件放在App_code文件夹中,也不会在第一次构建之前获得智能感知,因此无论如何都不会产生差异。)

方法二:(用于类库,其中启动项目是Web应用程序)

在类库中,App_Code文件夹并不特别,所以为了能够将帮助页面设置为全局,我们必须覆盖剃刀代码,因为它是硬编码的,只为代码中的代码创建全局帮助程序。 App_code文件夹。

此外,剃刀代码的设计使得对于全局帮助程序,它会根据完整路径创建名称空间,这可能是您不感兴趣的。

毕竟我们仍然遇到问题,没有可用的智能感知,所以为了避免所有这些问题,我编写了以下代码,假设:

  1. 您的.cshtml(或vbhtml)文件将被复制到最终项目输出目录
  2. 添加.cs(或.vb)文件,其名称与全局助手文件名相同,并将其构建操作设置为“compile”,(此文件将在启动时自动生成以提供智能感知)
  3. 您必须在AssemblyInfo.cs文件中注册PreApplicationStartupClass
  4. 您必须在PreApplicationStartupCode.Start()方法中替换,以依赖顺序为Bin文件夹中的全局帮助程序页面提供相对路径(即,如果其中一个全局帮助程序文件在另一个中使用帮助程序文件然后它应该在它之后列出)。
  5. 在CustomRazorCodeHost类中,您必须选择适合安装的MVC版本的PostProcessGeneratedCode()的正确方法
  6. 这是代码(但必须添加适当的“using”语句):

    [EditorBrowsable(EditorBrowsableState.Never)]
    public static class PreApplicationStartCode
    {
        private static bool _startWasCalled;
    
        public static void Start()
        {
            // Even though ASP.NET will only call each PreAppStart once, we sometimes internally call one PreAppStart from 
            // another PreAppStart to ensure that things get initialized in the right order. ASP.NET does not guarantee the 
            // order so we have to guard against multiple calls.
            // All Start calls are made on same thread, so no lock needed here.
    
            if (_startWasCalled)
            {
                return;
            }
            _startWasCalled = true;
    
            //Add here the the global helpers based on dependency
            //also note that each global helper should have a .cs file in the project with the same name
            CustomRazorHelperBuildProvider bp = new CustomRazorHelperBuildProvider();
            bp.VirtualPath = "~/Bin/path/to/helpers/file/Helpers.cshtml";
            bp.GenerateCodeAndCompile();
    
            bp = new CustomRazorHelperBuildProvider();
            bp.VirtualPath = "~/Bin/path/to/helpers/file/DepndentHelpers.cshtml";
            bp.GenerateCodeAndCompile();
        }
    }
    
    public class CustomRazorHelperBuildProvider :RazorBuildProvider
    {
        static List<string> GeneratedAssemblyReferences = new List<string>();
        public new string VirtualPath { get; set; }
        protected override System.Web.WebPages.Razor.WebPageRazorHost CreateHost()
        {
            return new CustomCodeRazorHost(VirtualPath);
        }
        private WebPageRazorHost _host;
        internal WebPageRazorHost Host
        {
            get
            {
                if (_host == null)
                {
                    _host = CreateHost();
                }
                return _host;
            }            
        }
        private CodeCompileUnit _generatedCode = null;
        internal CodeCompileUnit GeneratedCode
        {
            get
            {
                if (_generatedCode == null)
                {
                    EnsureGeneratedCode();
                }
                return _generatedCode;
            }
        }
        private CodeDomProvider _provider = null;
        internal CodeDomProvider Provider
        {
            get
            {
                if(_provider == null)
                {
                    _provider = GetProvider();
                }
                return _provider;
            }
        }
        private void EnsureGeneratedCode()
        {
            RazorTemplateEngine engine = new RazorTemplateEngine(Host);
            GeneratorResults results = null;
            using (TextReader reader = OpenReader(VirtualPath))
            {
                results = engine.GenerateCode(reader, className: null, rootNamespace: null, sourceFileName: Host.PhysicalPath);
            }
            if (!results.Success)
            {
                RazorError error = results.ParserErrors.Last();
                throw new HttpParseException(error.Message + Environment.NewLine, null, VirtualPath, null, error.Location.LineIndex + 1);
            }
            _generatedCode = results.GeneratedCode;
        }
        private CodeDomProvider GetProvider()
        {
            CompilerType compilerType = GetDefaultCompilerTypeForLanguage(Host.CodeLanguage.LanguageName);
            CodeDomProvider provider = CreateCodeDomProviderWithPropertyOptions(compilerType.CodeDomProviderType);
            return provider;
        }
    
        /// <summary>
        /// Generates the c# (or vb.net) code, for the intellisense to work
        /// </summary>
        public void GenerateCode()
        {
            //Remember that if there is a razor error, then the next time the project will not compile at all, because the generated .cs file will also have the error!
            //The solution is to add a pre-build event to truncate the file, but not remove it!, also note that the pre-build event will not work in time if the .cs file is open in the VS editor!
            string filePath = VirtualPath.Replace("/", "\\").Replace("~\\Bin", "").Replace("\\Debug", "").Replace("\\Release", "");
            filePath = filePath.Remove(filePath.Length - 4);
            //filePath = filePath.Insert(filePath.LastIndexOf("\\"), "\\HelperAutoGeneratedCode");            
            Assembly curAssem = Assembly.GetExecutingAssembly();
            filePath = HttpRuntime.AppDomainAppPath + "\\..\\" + curAssem.GetName().Name + filePath;
    
            using (FileStream fs = new FileStream(filePath, FileMode.Truncate))
            {
                using (StreamWriter sw = new StreamWriter(fs))
                {
                    Provider.GenerateCodeFromCompileUnit(GeneratedCode, sw, null);                    
                    sw.Flush();
                    sw.Close();
                }                
                fs.Close();
            }
            //We need to replace the type of the helpers from "HelperResult" to object, otherwise the intellisense will complain that "it can't convert from HelperResult to object"
            string text = File.ReadAllText(filePath);
            text = text.Replace("public static System.Web.WebPages.HelperResult ", "public static object ");
            File.WriteAllText(filePath, text); 
        }
    
        public void GenerateCodeAndCompile()
        {
            GenerateCode();
            Compile();
        }
    
        /// <summary>
        /// Compiles the helper pages for use at runtime
        /// </summary>
        /// <returns>Compiler Result</returns>
        public CompilerResults Compile()
        {
            Assembly assem = Assembly.GetExecutingAssembly();
            AssemblyName[] references = assem.GetReferencedAssemblies();
            List<string> referenceNames = references.Select(r => Assembly.ReflectionOnlyLoad(r.FullName).Location).ToList();
            referenceNames.Add(assem.Location);
    
            //Add here references that are not included in the project, but are needed for the generated assembly, you can see this through the results.Errors
            referenceNames.Add((typeof(WebMatrix.Data.ConnectionEventArgs).Assembly.Location));
            referenceNames.Add((typeof(WebMatrix.WebData.SimpleRoleProvider).Assembly.Location));
    
            if (GeneratedAssemblyReferences != null && GeneratedAssemblyReferences.Count > 0)
            {
                referenceNames.AddRange(GeneratedAssemblyReferences);
            }
    
            CompilerResults results = Provider.CompileAssemblyFromDom(new CompilerParameters(referenceNames.ToArray()), new CodeCompileUnit[] { GeneratedCode });
            if (results.Errors.HasErrors)
            {
                IEnumerator en = results.Errors.GetEnumerator();
                en.MoveNext();
                CompilerError error = en.Current as CompilerError;
                throw new HttpParseException(error.ErrorText + Environment.NewLine, null, VirtualPath, null, error.Line);
            }
            Assembly assemblyRef = GetGeneratedType(results).Assembly;
            GeneratedAssemblyReferences.Add(assemblyRef.Location); //So that any subsequent helper page that is dependent on it will have it as a reference
            //We need to make it available for Razor, so it will work with reguler razor pages at runtime
            RazorBuildProvider.CodeGenerationStarted += new EventHandler((sender, args) => (sender as RazorBuildProvider).AssemblyBuilder.AddCodeCompileUnit(this, GeneratedCode));
            return results;
        }
    
        private static CodeDomProvider CreateCodeDomProviderWithPropertyOptions(Type codeDomProviderType)
        {
            // The following resembles the code in System.CodeDom.CompilerInfo.CreateProvider
    
            // Make a copy to avoid modifying the original.
            var originalProviderOptions = GetProviderOptions(codeDomProviderType);
            IDictionary<string, string> providerOptions = null;
            if (originalProviderOptions != null)
            {
                providerOptions = new Dictionary<string, string>(originalProviderOptions);
            }
    
            AssemblyName[] references = Assembly.GetExecutingAssembly().GetReferencedAssemblies();
            foreach (AssemblyName reference in references)
            {
                if (reference.Name == "mscorlib")
                {
                    providerOptions["CompilerVersion"] = "v" + reference.Version.Major + "." + reference.Version.Minor;
                    break;
                }
            }
    
            if (providerOptions != null && providerOptions.Count > 0)
            {
                ConstructorInfo ci = codeDomProviderType.GetConstructor(new Type[] { typeof(IDictionary<string, string>) });
                CodeDomProvider provider = null;
                if (ci != null)
                {
                    // First, obtain the language for the given codedom provider type.
                    CodeDomProvider defaultProvider = (CodeDomProvider)Activator.CreateInstance(codeDomProviderType);
                    string extension = defaultProvider.FileExtension;
                    // Then, use the new createProvider API to create an instance.
                    provider = CodeDomProvider.CreateProvider(extension, providerOptions);
                }
                return provider;
            }
    
            return null;
        }
    
        internal static IDictionary<string, string> GetProviderOptions(Type codeDomProviderType)
        {
            // Using reflection to get the property for the time being.
            // This could simply return CompilerInfo.PropertyOptions if it goes public in future.
            CodeDomProvider provider = (CodeDomProvider)Activator.CreateInstance(codeDomProviderType);
            string extension = provider.FileExtension;
            if (CodeDomProvider.IsDefinedExtension(extension))
            {
                CompilerInfo ci = CodeDomProvider.GetCompilerInfo(CodeDomProvider.GetLanguageFromExtension(extension));
                PropertyInfo pi = ci.GetType().GetProperty("ProviderOptions",
                BindingFlags.NonPublic | BindingFlags.Public | BindingFlags.IgnoreCase | BindingFlags.Instance);
                if (pi != null)
                    return (IDictionary<string, string>)pi.GetValue(ci, null);
                return null;
            }
            return null;
        }
    }
    
     public class CustomCodeRazorHost : WebPageRazorHost
    {
        internal const string ApplicationInstancePropertyName = "ApplicationInstance";
        internal const string ContextPropertyName = "Context";
        internal const string WebDefaultNamespace = "ASP";
        private static readonly string _helperPageBaseType = typeof(HelperPage).FullName;
    
        public CustomCodeRazorHost(string virtualPath)
            : base(virtualPath)
        {
            DefaultBaseClass = _helperPageBaseType;
            DefaultNamespace = WebDefaultNamespace;
            DefaultDebugCompilation = false;
            StaticHelpers = true;
        }
    
        //Version for MVC 3
        public override void PostProcessGeneratedCode(CodeCompileUnit codeCompileUnit, CodeNamespace generatedNamespace, CodeTypeDeclaration generatedClass, CodeMemberMethod executeMethod)
        {
            // Add additional global imports
            generatedNamespace.Imports.AddRange(GetGlobalImports().Select(s => new CodeNamespaceImport(s)).ToArray());
    
            // Create ApplicationInstance property
            CodeMemberProperty prop = new CodeMemberProperty()
            {
                Name = ApplicationInstancePropertyName,
                Type = new CodeTypeReference(typeof(HttpApplication).FullName),
                HasGet = true,
                HasSet = false,
                Attributes = MemberAttributes.Family | MemberAttributes.Final
            };
            prop.GetStatements.Add(
                new CodeMethodReturnStatement(
                    new CodeCastExpression(
                        new CodeTypeReference(typeof(HttpApplication).FullName),
                        new CodePropertyReferenceExpression(
                            new CodePropertyReferenceExpression(
                                null,
                                ContextPropertyName),
                            ApplicationInstancePropertyName))));
            generatedClass.Members.Insert(0, prop);
    
            // Yank out the execute method (ignored in Razor Web Code pages)
            generatedClass.Members.Remove(executeMethod);
    
            // Make ApplicationInstance static
            CodeMemberProperty appInstanceProperty =
                generatedClass.Members
                    .OfType<CodeMemberProperty>()
                    .Where(p => ApplicationInstancePropertyName
                                    .Equals(p.Name))
                    .SingleOrDefault();
    
            if (appInstanceProperty != null)
            {
                appInstanceProperty.Attributes |= MemberAttributes.Static;
            }
        }
    
        //Version for MVC 4
        public override void PostProcessGeneratedCode(CodeGeneratorContext context)
        {
            // Add additional global imports
            context.Namespace.Imports.AddRange(GetGlobalImports().Select(s => new CodeNamespaceImport(s)).ToArray());
    
            // Create ApplicationInstance property
            CodeMemberProperty prop = new CodeMemberProperty()
            {
                Name = ApplicationInstancePropertyName,
                Type = new CodeTypeReference(typeof(HttpApplication).FullName),
                HasGet = true,
                HasSet = false,
                Attributes = MemberAttributes.Family | MemberAttributes.Final
            };
            prop.GetStatements.Add(
                new CodeMethodReturnStatement(
                    new CodeCastExpression(
                        new CodeTypeReference(typeof(HttpApplication).FullName),
                        new CodePropertyReferenceExpression(
                            new CodePropertyReferenceExpression(
                                null,
                                ContextPropertyName),
                            ApplicationInstancePropertyName))));
            context.GeneratedClass.Members.Insert(0, prop);
    
            // Yank out the execute method (ignored in Razor Web Code pages)
            context.GeneratedClass.Members.Remove(context.TargetMethod);
    
            // Make ApplicationInstance static
            CodeMemberProperty appInstanceProperty =
                context.GeneratedClass.Members
                    .OfType<CodeMemberProperty>()
                    .Where(p => ApplicationInstancePropertyName
                                    .Equals(p.Name))
                    .SingleOrDefault();
    
            if (appInstanceProperty != null)
            {
                appInstanceProperty.Attributes |= MemberAttributes.Static;
            }
        }
    
        protected override string GetClassName(string virtualPath)
        {
            return ParserHelpers.SanitizeClassName(Path.GetFileNameWithoutExtension(virtualPath));
        }
    } 
    

    但请注意,如果.cshtml文件中存在语法错误,则下次编译时会遇到问题(因为生成的.cs文件会出现编译错误),但是Visual Studio显然有问题可以查明问题。

    有时,上一次构建的编译代码(从.cs文件编译)有时会与新更新的.cshtml文件发生冲突。

    因此我建议添加一个预构建事件来截断文件

    echo. > $(ProjectDir)\Path\to\.cs\file
    

    你可以更复杂,只有当.cshtml文件被更改时才能这样做(这也适用于我上面编写的代码)。

答案 2 :(得分:4)

在包含帮助程序的视图上使用Razor Generator扩展名,您将在编译之前生成视图的代码。生成的视图代码是项目的一部分并编译到程序集中,因此您可以将视图文件放在任何位置,并在任何地方使用帮助程序,甚至可以从单元测试中使用。

答案 3 :(得分:-1)

当然,您可以将它们放在代码或项目结构中的任何位置。在您创建帮助程序的文件中,请确保使用System.Web.Mvc。

然后通常像这样扩展Helper类:

namespace System.Web.Mvc
{
    static class HtmlHelperExtensions
    {
        public static IHtmlString MyNewHelper(this HtmlHelper helper, string someParam)
        {
            // do something
        }
    }
}