Вопрос по asp.net-mvc-3 – Можно ли использовать глобальную бритву @helper вне App_Code?

21

Вопрос прост, как указано в заголовке: есть ли способ иметь помощников бритвы вне «App_Code»?

Пример (файл HtmlEx.cshtml):

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

Я спрашиваю об этом, потому что мне больше нечего добавить в App_Code; Я хочу структурировать свой проект немного по-другому.

Благодарю.

UPDATE: Я не хочу никаких других типов расширений. Меня интересуют только чисто бритвенные помощники, о чем Скотт говорит здесь:http://weblogs.asp.net/scottgu/archive/2011/05/12/asp-net-mvc-3-and-the-helper-syntax-within-razor.aspx

Я думаю, что ребята из Microsoft не должны путать понятия. Я действительно не вижу смысла в использовании App_Code в проекте MVC. Они заставляют нас размещать глобальные бритвенные представления там, и вместо этого я даже не могу поместить расширения внутрь (stackoverflow.com/questions/3686906/…). Странный! Learner
Я считаю, что причина помещения его в папку App_Code заключается в том, что это единственный способ получить intellisense для всего проекта yoel halb
также, когда он находится в App_Code, он создает их как статические методы и правильно подключает их, чтобы иметь возможность использовать все соответствующие контексты, где они обычно являются экземплярами. Simon_Weaver

Ваш Ответ

4   ответа
18

having razor helpers outside of 'App_Code'?

Нет, это не так.

Простой и эффективный ответ ... Спасибо ... Буду держать его открытым дольше, чтобы узнать, знает ли кто-нибудь какие-нибудь обходные пути. Learner
Простой, эффективный и неудачный ответ.
6

Never Say Never...

Method One: (For use in a web application project)

Просто добавьте событие перед сборкой, чтобы скопировать ваш файл в папку App_Code.

(Но так как файл, вероятно, должен быть включен в проект, вы можете добавить пустой файл с тем же именем в каталог App_Code, а затем создать событие build для его обновления.)

(Обратите внимание, что даже если вы поместите файл изначально в папку App_code, вы не получите intellisense до первого создания, так что в любом случае это не имеет значения.)

Method Two: (for use in a class library, in which the startup project is a web application)

В библиотеке классов папка App_Code не является чем-то особенным, поэтому для того, чтобы иметь глобальную страницу помощника, мы должны переопределить код бритвы, так как он жестко запрограммирован для создания глобальных помощников только для кода в папке App_code.

Более того, код бритвы разработан так, что для глобальных помощников он создает пространство имен на основе полного пути, что вас, вероятно, не интересует.

В конце концов мы остаемся с проблемой, что нет доступного intellisense, поэтому, чтобы избежать всех этих проблем, я написал следующий код, предполагая, что:

Your .cshtml (or vbhtml) files are getting copied to the final projects output directory You add a .cs (or .vb) file with the same name as the global helpers filename, and set it's build action to "compile", (this file will be autogenerated at startup to provide intellisense) You have to register the PreApplicationStartupClass in the AssemblyInfo.cs file You have to replace in the PreApplicationStartupCode.Start() method, to give the relative path to your global helpers page in the Bin folder, in the order of dependency (i.e. if one of the global helper files uses helpers in the other file then it should be listed after it). In the CustomRazorCodeHost class you have to pick the correct method of PostProcessGeneratedCode() that is appropriate for the MVC version installed

Вот код (но нужно будет добавить соответствующие операторы «используя»):

[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 был изменен (и это также относится к коду, который я написал выше).

+1 ты определенно заслуживаешь всех заслуг за это, но с точки зрения удобства я не уверен Learner
Хотя я сомневаюсь, что это полезно в стандартном веб-приложении, это, по-видимому, единственный способ использования глобальных помощников бритвы в библиотечном проекте.
4

Генератор бритвы расширение представления с помощниками внутри, и вы сгенерируете код для представления перед временем компиляции. Сгенерированный код представления является частью вашего проекта и компилируется в сборку, поэтому вы можете поместить файл представления в любом месте и использовать помощники в любом месте, даже из модульного теста.

Спасибо за это обновление ... Я попробую в какой-то момент Learner
плюс вам приходится каждый раз перекомпилировать, чтобы увидеть изменения, но это, вероятно, нормально для большинства помощников - просто боль во время разработки
это работает только для действительно простых представлений, если вы используете"~/" URL-адреса стиля или даже@Url.Content(...) это не сработает
-1

вы можете поместить их в любом месте вашего кода или структуры проекта. в файле, где вы создаете свой помощник, обязательно включите с помощью System.Web.Mvc.

тогда просто обычно расширяйте класс Helper следующим образом:

namespace System.Web.Mvc
{
    static class HtmlHelperExtensions
    {
        public static IHtmlString MyNewHelper(this HtmlHelper helper, string someParam)
        {
            // do something
        }
    }
}
извините, но я хочу бритвенных помощников, а не каких-либо других расширений (HtmlHelper и подобных) ... пожалуйста, проверьте это:weblogs.asp.net/scottgu/archive/2011/05/12/… Learner

Похожие вопросы