Can I have a global razor #helper outside of App_Code? - asp.net-mvc-3

The question is simple as stated in the title: Is there a way of having razor helpers outside of 'App_Code'?
Example ( HtmlEx.cshtml file ):
#helper Script(string fileName, UrlHelper url)
{
<script src="#url.Content("~/Scripts/" + fileName)" type="text/javascript"></script>
}
I ask this because I don't really have anything else to put in App_Code; I want to structure my project a bit different.
Thanks.
UPDATE: I don't want any other type of extensions. I am interested in only pure razor helpers as Scott is speaking about here: http://weblogs.asp.net/scottgu/archive/2011/05/12/asp-net-mvc-3-and-the-helper-syntax-within-razor.aspx

The question is simple as stated in the title: Is there a way of
having razor helpers outside of 'App_Code'?
No, there isn't.

Never Say Never...
Method One: (For use in a web application project)
Just add a pre-build event to copy your file into the App_Code folder.
(But since the file must probably be included in the project, you can add an empty file with the same name to the App_Code dir, and then have the build event to update it.)
(Note, that even if you put the file originally in the App_code folder you won't get intellisense until first time building, so it is anyway not a difference.)
Method Two: (for use in a class library, in which the startup project is a web application)
In a class library the App_Code folder is not anything special, so in order to be able to have the helper page global, we have to override the razor code, as it is hard coded to make global helpers only for code in the App_code folder.
Further more the razor code is designed so that for global helpers it creates a namespace based on the full path, something that you are probably not interested.
After all we remain with a problem, that there is no intellisense available, so to avoid all these problems, I have written the following code, assuming that:
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
Here is the code (but one will have to add the appropriate "using" statements):
[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));
}
}
But note that if there is a syntax error in the .cshtml file, you will have problems to compile next time (as the generated .cs file will have compile errors), however visual studio apparently has problems to pinpoint the problem.
Also sometimes the compiled code from the last build (compiled from the .cs file), can sometimes conflict with the newly updated .cshtml file.
Therefore I would recommend to add a pre-build event to truncate the file
echo. > $(ProjectDir)\Path\to\.cs\file
you can go more sophisticated and do it only if the .cshtml file has been changed (and this also applies to the code I have written above).

Use the Razor Generator extension on a view with helpers inside and you'll generate the code for the view before compilation time. The generated view code is part of your project and compiles into the assembly, so you can place the view file anywhere and use the helpers anywhere, even from a unit test.

sure, you can put them anywhere in your code or project structure. in the file where you create your helper be sure to include using System.Web.Mvc.
then just normally extend the Helper class like this:
namespace System.Web.Mvc
{
static class HtmlHelperExtensions
{
public static IHtmlString MyNewHelper(this HtmlHelper helper, string someParam)
{
// do something
}
}
}

Related

Calling Dynamics Web API with Entity metadata early binding

I would like to consume my organizations dynamics oData endpoint but with early bound classes. However, there are a lot of early bound tools out there and I wanted to know which one provides the best developer experience/least resistance?
For example, there is this one:
https://github.com/daryllabar/DLaB.Xrm.XrmToolBoxTools
https://github.com/yagasoft/DynamicsCrm-CodeGenerator
and so on. Is there a developer preference/method out there?
Early bound classes are for use with the Organization Service which is a SOAP service. The normal way to generate those classes is using CrmSvcUtil.
OData can be used in Organization Data Service or Web API, but those don't have Early Bound classes.
Further reading: Introducing the Microsoft Dynamics 365 web services
It's not impossible to use with standard SOAP Early bound class. We just have to be creative. If we work just with basic attributes (fields, not relationships, ecc) it seems possible. For example. for create and update, OData will not accept the entire early bounded class, just pass the attibutes:
class Program
{
static void Main(string[] args)
{
string token = System.Threading.Tasks.Task.Run(() => GetToken()).Result;
CRMWebAPI dynamicsWebAPI = new CRMWebAPI("https:/ORG.api.crm4.dynamics.com/api/data/v9.1/",
token);
CRMGetListOptions listOptions = new CRMGetListOptions
{
Select = new string[] { "EntitySetName" },
Filter = "LogicalName eq 'contact'"
};
dynamic entityDefinitions = dynamicsWebAPI.GetList<ExpandoObject>("EntityDefinitions", listOptions).Result;
Contact contact = new Contact
{
FirstName = "Felipe",
LastName = "Test",
MobilePhone = "38421254"
};
dynamic ret = System.Threading.Tasks.Task.Run(async () => await dynamicsWebAPI.Create(entityDefinitions.List[0].EntitySetName, KeyPairValueToObject(contact.Attributes))).Result;
}
public static async Task<string> GetToken()
{
string api = "https://ORG.api.crm4.dynamics.com/";
ClientCredential credential = new ClientCredential("CLIENT_ID", "CLIENT_SECRET");
AuthenticationContext authenticationContext = new AuthenticationContext("https://login.microsoftonline.com/commom/oauth2/authorize");
return authenticationContext.AcquireTokenAsync(api, credential).Result.AccessToken;
}
public static object KeyPairValueToObject(AttributeCollection keyValuePairs)
{
dynamic expando = new ExpandoObject();
var obj = expando as IDictionary<string, object>;
foreach (var keyValuePair in keyValuePairs)
obj.Add(keyValuePair.Key, keyValuePair.Value);
return obj;
}
}
It's a simple approach and I didn't went further.
Maybe we have to serealize other objects as OptionSets, DateTime (pass just the string) and EntityReferences but this simple test worked fine to me. I'm using Xrm.Tools.WebAPI and Microsoft.IdentityModel.Clients.ActiveDirectory. Maybe it's a way.
[Edit]
And so I decided to go and created a not well tested method to cast the attributes. Problems: We have to follow OData statments to use the API. To update/create an entity reference we can use this to reference https://www.inogic.com/blog/2016/02/set-values-of-all-data-types-using-web-api-in-dynamics-crm/
So
//To EntityReference
entityToUpdateOrCreate["FIELD_SCHEMA_NAME#odata.bind"] = "/ENTITY_SET_NAME(GUID)";
So, it's the Schema name, not field name. If you use CamelCase when set you fields name you'll have a problem where. We can resolve that with a (to that cute) code
public static object EntityToObject<T>(T entity) where T : Entity
{
dynamic expando = new ExpandoObject();
var obj = expando as IDictionary<string, object>;
foreach (var keyValuePair in entity.Attributes)
{
obj.Add(GetFieldName(entity, keyValuePair), CastEntityAttibutesValueOnDynamicObject(keyValuePair.Value));
}
return obj;
}
public static object CastEntityAttibutesValueOnDynamicObject(object attributeValue)
{
if (attributeValue.GetType().Name == "EntityReference")
{
CRMGetListOptions listOptions = new CRMGetListOptions
{
Select = new string[] { "EntitySetName" },
Filter = $"LogicalName eq '{((EntityReference)attributeValue).LogicalName}'"
};
dynamic entitySetName = dynamicsWebAPI.GetList<ExpandoObject>("EntityDefinitions", listOptions).Result.List[0];
return $"/{entitySetName.EntitySetName}({((EntityReference)attributeValue).Id})";
}
else if (attributeValue.GetType().Name == "OptionSetValue")
{
return ((OptionSetValue)attributeValue).Value;
}
else if (attributeValue.GetType().Name == "DateTime")
{
return ((DateTime)attributeValue).ToString("yyyy-MM-dd");
}
else if (attributeValue.GetType().Name == "Money")
{
return ((Money)attributeValue).Value;
}
else if (attributeValue.GetType().Name == "AliasedValue")
{
return CastEntityAttibutesValueOnDynamicObject(((AliasedValue)attributeValue).Value);
}
else
{
return attributeValue;
}
}
public static string GetFieldName<T>(T entity, KeyValuePair<string, object> keyValuePair) where T : Entity
{
switch (keyValuePair.Value.GetType().Name)
{
case "EntityReference":
var entityNameList = System.Threading.Tasks.Task.Run(async () => await dynamicsWebAPI.GetEntityDisplayNameList()).Result;
var firstEntity = entityNameList.Where(x => x.LogicalName == entity.LogicalName).FirstOrDefault();
var attrNameList = System.Threading.Tasks.Task.Run(async () => await dynamicsWebAPI.GetAttributeDisplayNameList(firstEntity.MetadataId)).Result;
return attrNameList.Where(x => x.LogicalName == keyValuePair.Key).Single().SchemaName + "#odata.bind";
case "ActivityParty":
throw new NotImplementedException(); //TODO
default:
return keyValuePair.Key;
}
}
Please, note that this approach do not seems fast or good in anyway. It's better if you have all this values as static so we can save some fetches
[Edit 2]
I just found on XRMToolBox a plugin called "Early bound generator for Web API" and it seems to be the best option. Maybe you should give it a try if you're still curious about that. I guess its the best approach.
The final code is this:
static void Main(string[] args)
{
string token = Task.Run(() => GetToken()).Result;
dynamicsWebAPI = new CRMWebAPI("https://ORG.api.crm4.dynamics.com/api/data/v9.1/",
token);
Contact contact = new Contact
{
FirstName = "Felipe",
LastName = "Test",
MobilePhone = "38421254",
new_Salutation = new EntityReference(new_salutation.EntitySetName, new Guid("{BFA27540-7BB9-E611-80EE-FC15B4281C8C}")),
BirthDate = new DateTime(1993, 04, 14),
};
dynamic ret = Task.Run(async () => await dynamicsWebAPI.Create(Contact.EntitySetName, contact.ToExpandoObject())).Result;
Contact createdContact = dynamicsWebAPI.Get<Contact>(Contact.EntitySetName, ret, new CRMGetListOptions
{
Select = new string[] { "*" }
}).Result;
}
and you have to change the ToExpandoObject on Entity.cs class (generated by the plugin)
public ExpandoObject ToExpandoObject()
{
dynamic expando = new ExpandoObject();
var expandoObject = expando as IDictionary<string, object>;
foreach (var attributes in Attributes)
{
if (attributes.Key == GetIdAttribute())
{
continue;
}
var value = attributes.Value;
var key = attributes.Key;
if (value is EntityReference entityReference)
{
value = $"/{entityReference.EntitySetName}({entityReference.EntityId})";
}
else
{
key = key.ToLower();
if (value is DateTime dateTimeValue)
{
var propertyForAttribute = GetPublicInstanceProperties().FirstOrDefault(x =>
x.Name.Equals(key, StringComparison.InvariantCultureIgnoreCase));
if (propertyForAttribute != null)
{
var onlyDateAttr = propertyForAttribute.GetCustomAttribute<OnlyDateAttribute>();
if (onlyDateAttr != null)
{
value = dateTimeValue.ToString(OnlyDateAttribute.Format);
}
}
}
}
expandoObject.Add(key, value);
}
return (ExpandoObject)expandoObject;
}
Links:
https://github.com/davidyack/Xrm.Tools.CRMWebAPI
https://www.xrmtoolbox.com/plugins/crm.webApi.earlyBoundGenerator/
We currently use XrmToolkit which has it's own version of early binding called ProxyClasses but will allow you to generate early binding using the CRM Service Utility (CrmSvcUtil). It does a lot more than just early binding which is why we use it on all of our projects but the early binding features alone would have me sold on it. in order to regenerate an entity definition all you do is right click the cs file in visual studio and select regenerate and it is done in a few seconds.
For my first 3 years of CRM development I used the XrmToolbox "Early Bound Generator" plugin which is really helpful as well.

MSBuild WriteCodeFragment Task

It's not clear to me what the purpose of the WriteCodeFragment task is (https://msdn.microsoft.com/en-us/library/microsoft.build.tasks.writecodefragment.aspx). Google just seems to turn-up documentation and the one example I've seen isn't definitive.
Can someone clarify? Thanks.
It looks like it's only good for populating a sourcecode file (of your choice of language) with attributes (read: to embed build information into a project).
This is the sourcecode for the task:
https://github.com/Microsoft/msbuild/blob/master/src/XMakeTasks/WriteCodeFragment.cs
The specific GenerateCode() method for reference. The type that it's principally enumerating looks to be attribute-specific (read: declarative and non-functional):
/// <summary>
/// Generates the code into a string.
/// If it fails, logs an error and returns null.
/// If no meaningful code is generated, returns empty string.
/// Returns the default language extension as an out parameter.
/// </summary>
[SuppressMessage("Microsoft.Globalization", "CA1305:SpecifyIFormatProvider", MessageId = "System.IO.StringWriter.#ctor(System.Text.StringBuilder)", Justification = "Reads fine to me")]
private string GenerateCode(out string extension)
{
extension = null;
bool haveGeneratedContent = false;
CodeDomProvider provider;
try
{
provider = CodeDomProvider.CreateProvider(Language);
}
catch (ConfigurationException ex)
{
Log.LogErrorWithCodeFromResources("WriteCodeFragment.CouldNotCreateProvider", Language, ex.Message);
return null;
}
catch (SecurityException ex)
{
Log.LogErrorWithCodeFromResources("WriteCodeFragment.CouldNotCreateProvider", Language, ex.Message);
return null;
}
extension = provider.FileExtension;
CodeCompileUnit unit = new CodeCompileUnit();
CodeNamespace globalNamespace = new CodeNamespace();
unit.Namespaces.Add(globalNamespace);
// Declare authorship. Unfortunately CodeDOM puts this comment after the attributes.
string comment = ResourceUtilities.FormatResourceString("WriteCodeFragment.Comment");
globalNamespace.Comments.Add(new CodeCommentStatement(comment));
if (AssemblyAttributes == null)
{
return String.Empty;
}
// For convenience, bring in the namespaces, where many assembly attributes lie
globalNamespace.Imports.Add(new CodeNamespaceImport("System"));
globalNamespace.Imports.Add(new CodeNamespaceImport("System.Reflection"));
foreach (ITaskItem attributeItem in AssemblyAttributes)
{
CodeAttributeDeclaration attribute = new CodeAttributeDeclaration(new CodeTypeReference(attributeItem.ItemSpec));
// Some attributes only allow positional constructor arguments, or the user may just prefer them.
// To set those, use metadata names like "_Parameter1", "_Parameter2" etc.
// If a parameter index is skipped, it's an error.
IDictionary customMetadata = attributeItem.CloneCustomMetadata();
List<CodeAttributeArgument> orderedParameters = new List<CodeAttributeArgument>(new CodeAttributeArgument[customMetadata.Count + 1] /* max possible slots needed */);
List<CodeAttributeArgument> namedParameters = new List<CodeAttributeArgument>();
foreach (DictionaryEntry entry in customMetadata)
{
string name = (string)entry.Key;
string value = (string)entry.Value;
if (name.StartsWith("_Parameter", StringComparison.OrdinalIgnoreCase))
{
int index;
if (!Int32.TryParse(name.Substring("_Parameter".Length), out index))
{
Log.LogErrorWithCodeFromResources("General.InvalidValue", name, "WriteCodeFragment");
return null;
}
if (index > orderedParameters.Count || index < 1)
{
Log.LogErrorWithCodeFromResources("WriteCodeFragment.SkippedNumberedParameter", index);
return null;
}
// "_Parameter01" and "_Parameter1" would overwrite each other
orderedParameters[index - 1] = new CodeAttributeArgument(String.Empty, new CodePrimitiveExpression(value));
}
else
{
namedParameters.Add(new CodeAttributeArgument(name, new CodePrimitiveExpression(value)));
}
}
bool encounteredNull = false;
for (int i = 0; i < orderedParameters.Count; i++)
{
if (orderedParameters[i] == null)
{
// All subsequent args should be null, else a slot was missed
encounteredNull = true;
continue;
}
if (encounteredNull)
{
Log.LogErrorWithCodeFromResources("WriteCodeFragment.SkippedNumberedParameter", i + 1 /* back to 1 based */);
return null;
}
attribute.Arguments.Add(orderedParameters[i]);
}
foreach (CodeAttributeArgument namedParameter in namedParameters)
{
attribute.Arguments.Add(namedParameter);
}
unit.AssemblyCustomAttributes.Add(attribute);
haveGeneratedContent = true;
}
StringBuilder generatedCode = new StringBuilder();
using (StringWriter writer = new StringWriter(generatedCode, CultureInfo.CurrentCulture))
{
provider.GenerateCodeFromCompileUnit(unit, writer, new CodeGeneratorOptions());
}
string code = generatedCode.ToString();
// If we just generated infrastructure, don't bother returning anything
// as there's no point writing the file
return haveGeneratedContent ? code : String.Empty;
}
That only other example that I found was this:
(Using WriteCodeFragment MSBuild Task)
<Target Name="BeforeBuild">
<ItemGroup>
<AssemblyAttributes Include="AssemblyVersion">
<_Parameter1>123.132.123.123</_Parameter1>
</AssemblyAttributes>
</ItemGroup>
<WriteCodeFragment Language="C#" OutputFile="BuildVersion.cs" AssemblyAttributes="#(AssemblyAttributes)" />
</Target>
Plugging this into an actual build renders:
//------------------------------------------------------------------------------
// <auto-generated>
// This code was generated by a tool.
// Runtime Version:4.0.30319.42000
//
// Changes to this file may cause incorrect behavior and will be lost if
// the code is regenerated.
// </auto-generated>
//------------------------------------------------------------------------------
using System;
using System.Reflection;
[assembly: AssemblyVersion("123.132.123.123")]
// Generated by the MSBuild WriteCodeFragment class.

Force tags to update from external event

this is a somewhat exotic question but I will try anyway.
I am writing a Visual Studio 2010 Extension using MEF. At some point in my code, I am required to provide some error glyphs ( like brakepoints ) whenever a document is saved. The problem is that I can't seem to find how to force GetTags of my ITagger to be called without having the user writing anything.
I can catch the document save event but I am missing the "link" between this and somehow getting GetTags method to be called. Any ideas?
Here is some code :
internal class RTextErrorTag : IGlyphTag
{}
class RTextErrorGlyphTagger : ITagger<RTextErrorTag>
{
private IClassifier mClassifier;
private SimpleTagger<ErrorTag> mSquiggleTagger;
private EnvDTE._DTE mMSVSInstance = null;
private List<object> mDTEEvents = new List<object>();
internal RTextErrorGlyphTagger(IClassifier classifier, IErrorProviderFactory squiggleProviderFactory, ITextBuffer buffer, IServiceProvider serviceProvider)
{
this.mClassifier = classifier;
this.mSquiggleTagger = squiggleProviderFactory.GetErrorTagger(buffer);
mMSVSInstance = serviceProvider.GetService(typeof(EnvDTE._DTE)) as EnvDTE._DTE;
var eventHolder = mMSVSInstance.Events.DocumentEvents;
mDTEEvents.Add(eventHolder);
eventHolder.DocumentSaved += new EnvDTE._dispDocumentEvents_DocumentSavedEventHandler(OnDocumentSaved);
}
void OnDocumentSaved(EnvDTE.Document Document)
{
//fire up event to renew glyphs - how to force GetTags method?
var temp = this.TagsChanged;
if (temp != null)
temp(this, new SnapshotSpanEventArgs(new SnapshotSpan() )); //how to foce get
}
IEnumerable<ITagSpan<RTextErrorTag>> ITagger<RTextErrorTag>.GetTags(NormalizedSnapshotSpanCollection spans)
{
foreach (SnapshotSpan span in spans)
{
foreach (var error in this.mSquiggleTagger.GetTaggedSpans(span))
{
yield return new TagSpan<RTextErrorTag>(new SnapshotSpan(span.Start, 1), new RTextErrorTag());
}
}
}
public event EventHandler<SnapshotSpanEventArgs> TagsChanged;
}

Why would running Pex Explorations result in ignoring a previously Pex-generated test method?

Code being tested:
public class TestReader
{
public string Content { get; private set; }
public void LoadFile(string fileName)
{
var content = FileSystem.ReadAllText(fileName);
if (!content.StartsWith("test"))
throw new ArgumentException("invalid file");
this.Content = content;
}
}
public static class FileSystem
{
public static string ReadAllText(string fileName)
{
return File.ReadAllText(fileName);
}
}
Pex method in test project:
[PexMethod]
public void CheckValidFileWithPex(string content)
{
// arrange
var fileName = "test.txt";
Moles_Example.Moles.MFileSystem.ReadAllTextString =
delegate(string f)
{
Assert.IsTrue(f == fileName); return content;
};
// act
var test = new TestReader();
test.LoadFile(fileName);
// assert
Assert.AreEqual(content, test.Content);
}
When I first run "Pex Explorations" on CheckValidFileWithPex(string content), five test methods are generated with the following values for content:
null
""
"\0\0\0\0"
"test"
"\0\0\0\0\0"
However, if I run "Pex Explorations" again, no changes made to the generated tests, existing test project code, or main project prior to second execution, then only 4 tests are listed as generated and the test input from item 3 ("\0\0\0\0") is missing.
The source code of the Pex-generated test file still has a test method for this case, but I do not understand why this case is not listed in Pex Exploration Results.
Thank you for your insight.

How to use Razor Section multiple times in a View & PartialView (merge) without overriding it?

In the _Layout.cshtml file, I have a section at the bottom of the body called "ScriptsContent" declared like this:
#RenderSection("ScriptsContent", required: false)
In my view, I can then use this section to add scripts to be executed. But what if I also have a PartialView that also need to use this section to add additional scripts?
View
#section ScriptsContent
{
<script type="text/javascript">
alert(1);
</script>
}
#Html.Partial("PartialView")
PartialView
#section ScriptsContent
{
<script type="text/javascript">
alert(2);
</script>
}
Result
Only the first script is rendered. The second script doesn't exist in source code of the webpage.
Razor seems to only output the first #section ScriptsContent that it sees. What I would like to know is if there's a way to merge each call to the section.
If we cannot do this, what do you propose?
Here's a solution for that problem. It's from this blog: http://blog.logrythmik.com/post/A-Script-Block-Templated-Delegate-for-Inline-Scripts-in-Razor-Partials.aspx
public static class ViewPageExtensions
{
private const string SCRIPTBLOCK_BUILDER = "ScriptBlockBuilder";
public static MvcHtmlString ScriptBlock(this WebViewPage webPage, Func<dynamic, HelperResult> template)
{
if (!webPage.IsAjax)
{
var scriptBuilder = webPage.Context.Items[SCRIPTBLOCK_BUILDER] as StringBuilder ?? new StringBuilder();
scriptBuilder.Append(template(null).ToHtmlString());
webPage.Context.Items[SCRIPTBLOCK_BUILDER] = scriptBuilder;
return new MvcHtmlString(string.Empty);
}
return new MvcHtmlString(template(null).ToHtmlString());
}
public static MvcHtmlString WriteScriptBlocks(this WebViewPage webPage)
{
var scriptBuilder = webPage.Context.Items[SCRIPTBLOCK_BUILDER] as StringBuilder ?? new StringBuilder();
return new MvcHtmlString(scriptBuilder.ToString());
}
}
so anywwhere in your View or PartialView you can use this:
#this.ScriptBlock(
#<script type='text/javascript'>
alert(1);
</script>
)
and in your _Layout or MasterView, use this:
#this.WriteScriptBlocks()
There is no way to share sections between a view and partial views.
Absent a ScriptManager-like solution, you could have a collection of script files (initialized in your view and stored either in HttpContext.Items or in ViewData) to which the partial view would append the script file names it requires. Then towards the end of your view you would declare a section that fetches that collection and emits the right script tags.
The problem with the accepted answer is that it breaks Output Caching. The trick to solving this is to overwrite the OutputCache attribute with your own implementation. Unfortunately we can't extend the original attribute since it has lots of internal methods which we need to access.
I actually use Donut Output Caching which overwrites the OutputCache attribute itself. There are alternative libraries which also use their own OutputCache attribute so I will explain the steps I made to get it to work so that you can apply it to whichever one you're using.
First you need to copy the existing OutputCache attribute and place it within your application. You can get the existing attribute by looking at the source code.
Now add the following property to the class. This is where we store the script blocks so we can render the correct ones when retrieving from the cache.
public static ConcurrentDictionary<string, StringBuilder> ScriptBlocks = new ConcurrentDictionary<string, StringBuilder>();
Now inside the OnActionExecuting method you need to store the cache key (the unique identifier for the output cache) inside the current requests collection. For example:
filterContext.HttpContext.Items["OutputCacheKey"] = cacheKey;
Now modify the ViewPageExtensions class by adding the following (replacing CustomOutputCacheAttribute with the name of your attribute):
var outputCacheKey = webPage.Context.Items["OutputCacheKey"] as string;
if (outputCacheKey != null)
CustomOutputCacheAttribute.ScriptBlocks.AddOrUpdate(outputCacheKey, new StringBuilder(template(null).ToHtmlString()), (k, sb) => {
sb.Append(template(null).ToHtmlString());
return sb;
});
before:
return new MvcHtmlString(string.Empty);
Note: For a slight performance boost you'll also want to make sure you only call "template(null).ToHtmlString()" once.
Now return to your custom OutputCache attribute and add the following only when you are retrieving from the cache inside the OnActionExecuting method:
if (ScriptBlocks.ContainsKey(cacheKey)) {
var scriptBuilder = filterContext.HttpContext.Items["ScriptBlockBuilder"] as StringBuilder ?? new StringBuilder();
scriptBuilder.Append(ScriptBlocks[cacheKey].ToString());
filterContext.HttpContext.Items["ScriptBlockBuilder"] = scriptBuilder;
}
Here's the final code of my attribute:
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Globalization;
using System.IO;
using System.Linq;
using System.Text;
using System.Web;
using System.Web.Mvc;
using System.Web.UI;
using DevTrends.MvcDonutCaching;
public class CustomOutputCacheAttribute : ActionFilterAttribute, IExceptionFilter {
private readonly IKeyGenerator _keyGenerator;
private readonly IDonutHoleFiller _donutHoleFiller;
private readonly IExtendedOutputCacheManager _outputCacheManager;
private readonly ICacheSettingsManager _cacheSettingsManager;
private readonly ICacheHeadersHelper _cacheHeadersHelper;
private bool? _noStore;
private CacheSettings _cacheSettings;
public int Duration { get; set; }
public string VaryByParam { get; set; }
public string VaryByCustom { get; set; }
public string CacheProfile { get; set; }
public OutputCacheLocation Location { get; set; }
public bool NoStore {
get { return _noStore ?? false; }
set { _noStore = value; }
}
public static ConcurrentDictionary<string, StringBuilder> ScriptBlocks = new ConcurrentDictionary<string, StringBuilder>();
public DonutOutputCacheAttribute() {
var keyBuilder = new KeyBuilder();
_keyGenerator = new KeyGenerator(keyBuilder);
_donutHoleFiller = new DonutHoleFiller(new EncryptingActionSettingsSerialiser(new ActionSettingsSerialiser(), new Encryptor()));
_outputCacheManager = new OutputCacheManager(OutputCache.Instance, keyBuilder);
_cacheSettingsManager = new CacheSettingsManager();
_cacheHeadersHelper = new CacheHeadersHelper();
Duration = -1;
Location = (OutputCacheLocation)(-1);
}
public override void OnActionExecuting(ActionExecutingContext filterContext) {
_cacheSettings = BuildCacheSettings();
var cacheKey = _keyGenerator.GenerateKey(filterContext, _cacheSettings);
if (_cacheSettings.IsServerCachingEnabled) {
var cachedItem = _outputCacheManager.GetItem(cacheKey);
if (cachedItem != null) {
filterContext.Result = new ContentResult {
Content = _donutHoleFiller.ReplaceDonutHoleContent(cachedItem.Content, filterContext),
ContentType = cachedItem.ContentType
};
if (ScriptBlocks.ContainsKey(cacheKey)) {
var scriptBuilder = filterContext.HttpContext.Items["ScriptBlockBuilder"] as StringBuilder ?? new StringBuilder();
scriptBuilder.Append(ScriptBlocks[cacheKey].ToString());
filterContext.HttpContext.Items["ScriptBlockBuilder"] = scriptBuilder;
}
}
}
if (filterContext.Result == null) {
filterContext.HttpContext.Items["OutputCacheKey"] = cacheKey;
var cachingWriter = new StringWriter(CultureInfo.InvariantCulture);
var originalWriter = filterContext.HttpContext.Response.Output;
filterContext.HttpContext.Response.Output = cachingWriter;
filterContext.HttpContext.Items[cacheKey] = new Action<bool>(hasErrors => {
filterContext.HttpContext.Items.Remove(cacheKey);
filterContext.HttpContext.Response.Output = originalWriter;
if (!hasErrors) {
var cacheItem = new CacheItem {
Content = cachingWriter.ToString(),
ContentType = filterContext.HttpContext.Response.ContentType
};
filterContext.HttpContext.Response.Write(_donutHoleFiller.RemoveDonutHoleWrappers(cacheItem.Content, filterContext));
if (_cacheSettings.IsServerCachingEnabled && filterContext.HttpContext.Response.StatusCode == 200)
_outputCacheManager.AddItem(cacheKey, cacheItem, DateTime.UtcNow.AddSeconds(_cacheSettings.Duration));
}
});
}
}
public override void OnResultExecuted(ResultExecutedContext filterContext) {
ExecuteCallback(filterContext, false);
if (!filterContext.IsChildAction)
_cacheHeadersHelper.SetCacheHeaders(filterContext.HttpContext.Response, _cacheSettings);
}
public void OnException(ExceptionContext filterContext) {
if (_cacheSettings != null)
ExecuteCallback(filterContext, true);
}
private void ExecuteCallback(ControllerContext context, bool hasErrors) {
var cacheKey = _keyGenerator.GenerateKey(context, _cacheSettings);
var callback = context.HttpContext.Items[cacheKey] as Action<bool>;
if (callback != null)
callback.Invoke(hasErrors);
}
private CacheSettings BuildCacheSettings() {
CacheSettings cacheSettings;
if (string.IsNullOrEmpty(CacheProfile)) {
cacheSettings = new CacheSettings {
IsCachingEnabled = _cacheSettingsManager.IsCachingEnabledGlobally,
Duration = Duration,
VaryByCustom = VaryByCustom,
VaryByParam = VaryByParam,
Location = (int)Location == -1 ? OutputCacheLocation.Server : Location,
NoStore = NoStore
};
} else {
var cacheProfile = _cacheSettingsManager.RetrieveOutputCacheProfile(CacheProfile);
cacheSettings = new CacheSettings {
IsCachingEnabled = _cacheSettingsManager.IsCachingEnabledGlobally && cacheProfile.Enabled,
Duration = Duration == -1 ? cacheProfile.Duration : Duration,
VaryByCustom = VaryByCustom ?? cacheProfile.VaryByCustom,
VaryByParam = VaryByParam ?? cacheProfile.VaryByParam,
Location = (int)Location == -1 ? ((int)cacheProfile.Location == -1 ? OutputCacheLocation.Server : cacheProfile.Location) : Location,
NoStore = _noStore.HasValue ? _noStore.Value : cacheProfile.NoStore
};
}
if (cacheSettings.Duration == -1)
throw new HttpException("The directive or the configuration settings profile must specify the 'duration' attribute.");
if (cacheSettings.Duration < 0)
throw new HttpException("The 'duration' attribute must have a value that is greater than or equal to zero.");
return cacheSettings;
}
}
I also had to modify the Donut Output Cache library to make IExtendedOutputCacheManager and the OutputCacheManager constructor public.
Please note this has been extracted from my application and may require some minor tweaks. You should also place WriteScriptBlocks at the bottom of the page so it is not called until after all child actions are triggered.
Hope this helps.

Resources