T-SQL CLR: Can you CREATE AGGREGATE with optional parameter(s)? - sql-server-2008

I'm hoping to be able to allow optional parameters to be specified, so I can overload the Accumulate() method, can it be done?
I'd like to overload to allow a delimiter to specified, I've seen others where it must force a delimiter to be specified but this behaviour doesn't suit.
CREATE AGGREGATE dbo.Concatenate (#input nvarchar(max), <OPTIONAL PARAMETER HERE>)
RETURNS nvarchar(max)
For reference, here is the aggregate class code that contains the Accumulate() method I'm looking to overload:
using System;
using System.Data;
using Microsoft.SqlServer.Server;
using System.Data.SqlTypes;
using System.IO;
using System.Text;
namespace CLR.Utilities {
/// <summary>
/// <list type="references">
/// <reference>
/// <name>How to: Create and Run a CLR SQL Server Aggregate</name>
/// <link>http://msdn.microsoft.com/en-us/library/91e6taax(v=vs.90).aspx</link>
/// </reference>
/// <reference>
/// <name>SqlUserDefinedAggregateAttribute</name>
/// <link>http://msdn.microsoft.com/en-us/library/microsoft.sqlserver.server.sqluserdefinedaggregateattribute(v=vs.90).aspx</link>
/// </reference>
/// <reference>
/// <name>Invoking CLR User-Defined Aggregate Functions (Provides seed code for this function)</name>
/// <link>http://technet.microsoft.com/en-us/library/ms131056.aspx</link>
/// </reference>
/// </list>
/// </summary>
[Serializable]
[SqlUserDefinedAggregate(
Format.UserDefined, //use clr serialization to serialize the intermediate result
IsInvariantToNulls = true, //optimizer property
IsInvariantToDuplicates = false, //optimizer property
IsInvariantToOrder = false, //optimizer property
MaxByteSize = -1) //no maximum size in bytes of persisted value
]
public class Concatenate : IBinarySerialize {
/// <summary>
/// The variable that holds the intermediate result of the concatenation
/// </summary>
private StringBuilder intermediateResult;
/// <summary>
/// Initialize the internal data structures
/// </summary>
public void Init() {
this.intermediateResult = new StringBuilder();
}
/// <summary>
/// Accumulate the next value, not if the value is null
/// </summary>
/// <param name="value"></param>
public void Accumulate([SqlFacet(MaxSize = -1)] SqlString value) {
if (value.IsNull) {
return;
}
this.intermediateResult.Append(value.Value.Trim()).Append(',');
}
/// <summary>
/// Merge the partially computed aggregate with this aggregate.
/// </summary>
/// <param name="other"></param>
public void Merge(Concatenate other) {
this.intermediateResult.Append(other.intermediateResult);
}
/// <summary>
/// Called at the end of aggregation, to return the results of the aggregation.
/// </summary>
/// <returns></returns>
[return: SqlFacet(MaxSize = -1)]
public SqlString Terminate() {
string output = string.Empty;
//delete the trailing comma, if any
if (this.intermediateResult != null
&& this.intermediateResult.Length > 0) {
output = this.intermediateResult.ToString(0, this.intermediateResult.Length - 1).Trim();
}
return new SqlString(output);
}
public void Read(BinaryReader r) {
intermediateResult = new StringBuilder(r.ReadString());
}
public void Write(BinaryWriter w) {
w.Write(this.intermediateResult.ToString().Trim());
}
}
}
And here is the code for deployment that I'd like to modify also if optional parameters can be set:
IF EXISTS (SELECT * FROM sys.objects WHERE object_id = OBJECT_ID(N'[dbo].[Concatenate]') AND type = N'AF')
DROP AGGREGATE [dbo].[Concatenate]
GO
IF EXISTS (SELECT * FROM sys.assemblies asms WHERE asms.name = N'CLR.Utilities' and is_user_defined = 1)
DROP ASSEMBLY [CLR.Utilities]
GO
ALTER DATABASE [DatabaseName] SET TRUSTWORTHY ON
GO
CREATE ASSEMBLY [CLR.Utilities] FROM 'C:\Path\To\File\CLR.Utilities.dll' WITH PERMISSION_SET = UNSAFE
GO
CREATE AGGREGATE [dbo].[Concatenate] (#input nvarchar(max)) RETURNS nvarchar(max)
EXTERNAL NAME [CLR.Utilities].[CLR.Utilities.Concatenate]
GO
GRANT EXECUTE ON [dbo].[Concatenate] TO PUBLIC
GO

You could, however, make the parameter mandatory but optionally null. And, in your C# code test whether the value is null and act accordingly. Something like:
public void Accumulate([SqlFacet(MaxSize = -1)] SqlString value, SqlString delimiter) {
string _delimiter;
if (value.IsNull) {
return;
}
if (delimiter.IsNull) {
_delimiter = string.Empty;
}
else {
_delimiter = Delimiter.Value;
}
this.intermediateResult.Append(value.Value.Trim()).Append(_delimiter);
}

As far as I know, there's no way to make clr function or aggregate with optional parameters and that's sad.

There is a way, though it isn't nice. You simple add the delimiter to the end of the column you're concatenating with a char(0) between them. then in the clr, you can look for char(0) and grab the delimiter to use.
-- concat columns delimited by a comma
select grp,dbo.clrConcat(surname) as names
from people
-- concat column delimited by specified character
select grp,dbo.clrConcat(surname + char(0) + '|') as names
from people
Then in the clr code
''' <summary>
''' Initialize the internal data structures
''' </summary>
Public Sub Init()
Me.intermediateResult = New StringBuilder()
delimiter = ","
End Sub
''' <summary>
''' Accumulate the next value, not if the value is null
''' </summary>
''' <param name="value"></param>
Public Sub Accumulate(ByVal value As SqlString)
Dim Str As String
Dim Pos As Integer
If value.IsNull Then Return
Pos = value.Value.IndexOf(Chr(0))
If Pos >= 0 Then
Str = value.Value.Substring(0, Pos)
If Pos + 1 < value.Value.Length Then
delimiter = value.Value.Substring(Pos + 1, 1)
End If
Else
Str = value.Value
End If
Me.intermediateResult.Append(Str).Append(delimiter)
End Sub

Related

Is it possible to change the value of a variable in SSIS package using Script Task?

Is it possible to change the value of a variable in SSIS package using Script Task? I have a foreach loop container that does not want to read int variable, but I'm using that variable in Script Task and it needs to be int for the task I'm trying to accomplish. Every time I run the code I get the error: "Error: The type of the value (String) being assigned to variable "User::empid" differs from the current variable type (Int32). Variables may not change type during execution. Variable types are strict, except for variables of type Object." Is anyone familiar with an error like this one?
public void Main()
{
string filename;
// string datepart;
//bool FolderExistFlg;
//filename = Dts.Variables["User::name"].Value.ToString();
filename = System.IO.Path.GetFileName(Dts.Variables["User::name"].Value.ToString());
// datepart = (filename.Substring(filename.Length - 12)).Substring(0, 8);
string getEmployerId = filename.Split('_')[0];
int employerId = Int32.Parse(Regex.Match(getEmployerId, #"\d+").Value);
Dts.TaskResult = (int)ScriptResults.Success;
Dts.Variables["User::empId"].Value = employerId;
}
#region ScriptResults declaration
/// <summary>
/// This enum provides a convenient shorthand within the scope of this class for setting the
/// result of the script.
///
/// This code was generated automatically.
/// </summary>
enum ScriptResults
{
Success = Microsoft.SqlServer.Dts.Runtime.DTSExecResult.Success,
Failure = Microsoft.SqlServer.Dts.Runtime.DTSExecResult.Failure
};
#endregion
}

ASP.NET View Components: Remove default path to view

I want to return a View with a specific path to the view like so, return View(~/Views/Home). The type to be returned is an IViewComponentResult.
However, when the site gets rendered, it tries to find the view under: Components/{controller-name}/~/Views/Home.
So I want to ask you guys, if you know a smart way to delete Components/{controller-name}/ from the path. My view component class is deriving from the ViewComponent class.
The problem regarding this is that right now, you have to have a "Components" folder with the name of the controller as a subfolder which contains a Default.cshtml file. I do not like to have all my components inside one folder. I hope you have a solution.
The convention happened in ViewViewComponentResult hence if you don't want the convention, you will have to implement your own IViewComponentResult
since mvc is open source, you could copy all the ViewViewComponentResult and then change the convention bit.
so essentially two things has to be done:
create your own IViewComponentResult - lets call it MyViewViewComponentResult
create a helper in your implemented ViewComponent to replace the original View() - purpose is to returns the custom MyViewViewComponentResult you created at step 1
create your own IViewComponentResult
the convention happened at ExecuteAsync:
public class ViewViewComponentResult : IViewComponentResult
{
// {0} is the component name, {1} is the view name.
private const string ViewPathFormat = "Components/{0}/{1}";
private const string DefaultViewName = "Default";
....
public async Task ExecuteAsync(ViewComponentContext context)
{
....
if (result == null || !result.Success)
{
// This will produce a string like:
//
// Components/Cart/Default
//
// The view engine will combine this with other path info to search paths like:
//
// Views/Shared/Components/Cart/Default.cshtml
// Views/Home/Components/Cart/Default.cshtml
// Areas/Blog/Views/Shared/Components/Cart/Default.cshtml
//
// This supports a controller or area providing an override for component views.
var viewName = isNullOrEmptyViewName ? DefaultViewName : ViewName;
var qualifiedViewName = string.Format(
CultureInfo.InvariantCulture,
ViewPathFormat,
context.ViewComponentDescriptor.ShortName,
viewName);
result = viewEngine.FindView(viewContext, qualifiedViewName, isMainPage: false);
}
....
}
.....
}
so change it to your need
create a helper in your implemented ViewComponent which replace the original View
so base on original
/// <summary>
/// Returns a result which will render the partial view with name <paramref name="viewName"/>.
/// </summary>
/// <param name="viewName">The name of the partial view to render.</param>
/// <param name="model">The model object for the view.</param>
/// <returns>A <see cref="ViewViewComponentResult"/>.</returns>
public ViewViewComponentResult View<TModel>(string viewName, TModel model)
{
var viewData = new ViewDataDictionary<TModel>(ViewData, model);
return new ViewViewComponentResult
{
ViewEngine = ViewEngine,
ViewName = viewName,
ViewData = viewData
};
}
you would do something like
/// <summary>
/// Returns a result which will render the partial view with name <paramref name="viewName"/>.
/// </summary>
/// <param name="viewName">The name of the partial view to render.</param>
/// <param name="model">The model object for the view.</param>
/// <returns>A <see cref="ViewViewComponentResult"/>.</returns>
public MyViewViewComponentResult MyView<TModel>(string viewName, TModel model)
{
var viewData = new ViewDataDictionary<TModel>(ViewData, model);
return new MyViewViewComponentResult
{
ViewEngine = ViewEngine,
ViewName = viewName,
ViewData = viewData
};
}
so in future you would call MyView() instead of View()
more info check out another SO: Change component view location in Asp.Net 5

Passing Enum flag from WCF to json client

I have an Enum defined in a C# dll with the [Flags] attribute:
[Flags]
[DataContract(Namespace = "MyApp")]
public enum MobileNotifications
{
/// <summary>
///
/// </summary>
[EnumMember]
None,
/// <summary>
///
/// </summary>
[EnumMember]
Msg1,
/// <summary>
///
/// </summary>
[EnumMember]
Msg2,
/// <summary>
///
/// </summary>
[EnumMember]
Msg3,
/// <summary>
///
/// </summary>
[EnumMember]
Msg4
}
When I call a method "GetFlags", it returns the value as a single Int value.
MobileNotifications GetNotifications()
{
return Msg1 | Msg4;
}
The method above returns 5 as a value. Is there a way for WCF to pass an enum flag that can be parsed for json?
This is a .NET 4.0 WCF service.
First, you need to define explicit enumeration constants in powers of two to avoid any overlapping of differnt value combinations (see http://msdn.microsoft.com/en-gb/library/vstudio/system.flagsattribute.aspx). In order to return that to a Json client in a meaningful way, I'd suggest to ToString() it which actually creates a comma-separated list of the actual member names (which you can then parse on the client side). Example:
[Flags]
public enum MobileNotifications
{
None = 0,
Msg1 = 1,
Msg2 = 2,
Msg3 = 4
}
var value = MobileNotifications.Msg1 | MobileNotifications.Msg3;
var asString = string.Format("{0}", value); // will create: "Msg1, Msg3"
Personally I would have another look at this design. What does 5 mean? It can be made up a couple of different combinations can't it - Msg 1 | Msg4, Msg 2| Msg 3 etc.. The same goes for the rest. Can the consumer differentiate between the combinations? If not I would suggest making this more explicit or actually changing the enum values so that the combinations are unique such as
None =0,
Msg1 = 1,
Msg2 = 2,
Msg3 = 4,
Msg4 = 8

There is already an open DataReader associated with this Connection

I USE EF+MySql,the database contain two foriegnkey, and when i run my project,it say:There is already an open DataReader associated with this Connection which must be closed first, then i add MultipleActiveResultSets=true to connection string in web.config, i try again, it show me: The format of the initialization string does not meet specifications, how i can
fix this problem? the code difinite foriegnkey and the wrong initialization code list as below:
#region EDM 关系源元数据
[assembly: EdmRelationshipAttribute("blogModel", "cid", "cls", System.Data.Metadata.Edm.RelationshipMultiplicity.One, typeof(blog.Models.cls), "news", System.Data.Metadata.Edm.RelationshipMultiplicity.Many, typeof(blog.Models.news), true)]
[assembly: EdmRelationshipAttribute("blogModel", "uid", "users", System.Data.Metadata.Edm.RelationshipMultiplicity.One, typeof(blog.Models.users), "news", System.Data.Metadata.Edm.RelationshipMultiplicity.Many, typeof(blog.Models.news), true)]
#endregion
#region 导航属性
/// <summary>
/// 没有元数据文档可用。
/// </summary>
[XmlIgnoreAttribute()]
[SoapIgnoreAttribute()]
[DataMemberAttribute()]
[EdmRelationshipNavigationPropertyAttribute("blogModel", "cid", "cls")]
public cls cls
{
get
{
return ((IEntityWithRelationships)this).RelationshipManager.GetRelatedReference<cls>("blogModel.cid", "cls").Value;
}
set
{
((IEntityWithRelationships)this).RelationshipManager.GetRelatedReference<cls>("blogModel.cid", "cls").Value = value;
}
}
/// <summary>
/// 没有元数据文档可用。
/// </summary>
[BrowsableAttribute(false)]
[DataMemberAttribute()]
public EntityReference<cls> clsReference
{
get
{
return ((IEntityWithRelationships)this).RelationshipManager.GetRelatedReference<cls>("blogModel.cid", "cls");
}
set
{
if ((value != null))
{
((IEntityWithRelationships)this).RelationshipManager.InitializeRelatedReference<cls>("blogModel.cid", "cls", value);
}
}
}
/// <summary>
/// 没有元数据文档可用。
/// </summary>
[XmlIgnoreAttribute()]
[SoapIgnoreAttribute()]
[DataMemberAttribute()]
[EdmRelationshipNavigationPropertyAttribute("blogModel", "uid", "users")]
public users users
{
get
{
return ((IEntityWithRelationships)this).RelationshipManager.GetRelatedReference<users>("blogModel.uid", "users").Value;
}
set
{
((IEntityWithRelationships)this).RelationshipManager.GetRelatedReference<users>("blogModel.uid", "users").Value = value;
}
}
/// <summary>
/// 没有元数据文档可用。
/// </summary>
[BrowsableAttribute(false)]
[DataMemberAttribute()]
public EntityReference<users> usersReference
{
get
{
return ((IEntityWithRelationships)this).RelationshipManager.GetRelatedReference<users>("blogModel.uid", "users");
}
set
{
if ((value != null))
{
((IEntityWithRelationships)this).RelationshipManager.InitializeRelatedReference<users>("blogModel.uid", "users", value);
}
}
}
#endregion
public partial class blogEntities : ObjectContext
{
#region 构造函数
/// <summary>
/// 请使用应用程序配置文件的“blogEntities”部分中的连接字符串初始化新 blogEntities 对象。
/// </summary>
public blogEntities() : base("name=blogEntities", "blogEntities")
{
this.ContextOptions.LazyLoadingEnabled = true;
OnContextCreated();
}
/// <summary>
/// 初始化新的 blogEntities 对象。
/// </summary>
public blogEntities(string connectionString) : base(connectionString, "blogEntities")
{
this.ContextOptions.LazyLoadingEnabled = true;
OnContextCreated();
}
/// <summary>
/// 初始化新的 blogEntities 对象。
/// </summary>
public blogEntities(EntityConnection connection) : base(connection, "blogEntities")
{
this.ContextOptions.LazyLoadingEnabled = true;
OnContextCreated();
}
#endregion
As I know MultipleActiveResultSets are not available on MySql so you cannot add it to connection string (it is for MS SQL).
The reason for this problem is most probably lazy loading. You are iterating result of some query and in the same time you access not loaded navigation properties inside the loop. That requires additional query to be executed and that query requires a new data reader (the first one is still not closed because you are just iterating its result set).
The solution:
Either materiealize whole result set of the query you want to iterate by using ToList or perhaps also AsEnumerable
Or eager load navigation properties you want to use inside the loop by using Include
Use different Open and close SQL connection names for each of the DataReaders. This will solve the issue.
As you are using same open and close database connection for multiple DataReader which is not supported by SQL server in VB.NET.

Using sections in Editor/Display templates

I want to keep all of my JavaScript code in one section; just before the closing body tag in my master layout page and just wondering the best to go about it, MVC style.
For example, if I create a DisplayTemplate\DateTime.cshtml file which uses jQuery UI's DateTime Picker than I would embed the JavaScript directly into that template but then it will render mid-page.
In my normal views I can just use #section JavaScript { //js here } and then #RenderSection("JavaScript", false) in my master layout but this doesn't seem to work in display/editor templates - any ideas?
You could proceed with a conjunction of two helpers:
public static class HtmlExtensions
{
public static MvcHtmlString Script(this HtmlHelper htmlHelper, Func<object, HelperResult> template)
{
htmlHelper.ViewContext.HttpContext.Items["_script_" + Guid.NewGuid()] = template;
return MvcHtmlString.Empty;
}
public static IHtmlString RenderScripts(this HtmlHelper htmlHelper)
{
foreach (object key in htmlHelper.ViewContext.HttpContext.Items.Keys)
{
if (key.ToString().StartsWith("_script_"))
{
var template = htmlHelper.ViewContext.HttpContext.Items[key] as Func<object, HelperResult>;
if (template != null)
{
htmlHelper.ViewContext.Writer.Write(template(null));
}
}
}
return MvcHtmlString.Empty;
}
}
and then in your _Layout.cshtml:
<body>
...
#Html.RenderScripts()
</body>
and somewhere in some template:
#Html.Script(
#<script src="#Url.Content("~/Scripts/jquery-1.4.4.min.js")" type="text/javascript"></script>
)
Modified version of Darin's answer to ensure ordering. Also works with CSS:
public static IHtmlString Resource(this HtmlHelper HtmlHelper, Func<object, HelperResult> Template, string Type)
{
if (HtmlHelper.ViewContext.HttpContext.Items[Type] != null) ((List<Func<object, HelperResult>>)HtmlHelper.ViewContext.HttpContext.Items[Type]).Add(Template);
else HtmlHelper.ViewContext.HttpContext.Items[Type] = new List<Func<object, HelperResult>>() { Template };
return new HtmlString(String.Empty);
}
public static IHtmlString RenderResources(this HtmlHelper HtmlHelper, string Type)
{
if (HtmlHelper.ViewContext.HttpContext.Items[Type] != null)
{
List<Func<object, HelperResult>> Resources = (List<Func<object, HelperResult>>)HtmlHelper.ViewContext.HttpContext.Items[Type];
foreach (var Resource in Resources)
{
if (Resource != null) HtmlHelper.ViewContext.Writer.Write(Resource(null));
}
}
return new HtmlString(String.Empty);
}
You can add JS and CSS resources like this:
#Html.Resource(#<script src="#Url.Content("~/Scripts/jquery-1.4.4.min.js")" type="text/javascript"></script>, "js")
#Html.Resource(#<link rel="stylesheet" href="#Url.Content("~/CSS/style.css")" />, "css")
And render JS and CSS resources like this:
#Html.RenderResources("js")
#Html.RenderResources("css")
You could do a string check to see if it starts with script/link so you don't have to explicitly define what each resource is.
I faced the same problem, but solutions proposed here work good only for adding reference to the resource and are not very suitable for inline JS code. I found a very helpful article and wrapped all my inline JS (and also script tags) in
#using (Html.BeginScripts())
{
<script src="#Url.Content("~/Scripts/jquery-ui-1.8.18.min.js")" type="text/javascript"></script>
<script>
// my inline scripts here
<\script>
}
And in the _Layout view placed #Html.PageScripts() just before closing 'body' tag. Works like a charm for me.
The helpers themselves:
public static class HtmlHelpers
{
private class ScriptBlock : IDisposable
{
private const string scriptsKey = "scripts";
public static List<string> pageScripts
{
get
{
if (HttpContext.Current.Items[scriptsKey] == null)
HttpContext.Current.Items[scriptsKey] = new List<string>();
return (List<string>)HttpContext.Current.Items[scriptsKey];
}
}
WebViewPage webPageBase;
public ScriptBlock(WebViewPage webPageBase)
{
this.webPageBase = webPageBase;
this.webPageBase.OutputStack.Push(new StringWriter());
}
public void Dispose()
{
pageScripts.Add(((StringWriter)this.webPageBase.OutputStack.Pop()).ToString());
}
}
public static IDisposable BeginScripts(this HtmlHelper helper)
{
return new ScriptBlock((WebViewPage)helper.ViewDataContainer);
}
public static MvcHtmlString PageScripts(this HtmlHelper helper)
{
return MvcHtmlString.Create(string.Join(Environment.NewLine, ScriptBlock.pageScripts.Select(s => s.ToString())));
}
}
I liked the solution posted by #john-w-harding, so I combined it with the answer by #darin-dimitrov to make the following probably overcomplicated solution that lets you delay rendering any html (scripts too) within a using block.
USAGE
In a repeated partial view, only include the block one time:
#using (Html.Delayed(isOnlyOne: "MYPARTIAL_scripts")) {
<script>
someInlineScript();
</script>
}
In a (repeated?) partial view, include the block for every time the partial is used:
#using (Html.Delayed()) {
<b>show me multiple times, #Model.Whatever</b>
}
In a (repeated?) partial view, include the block once, and later render it specifically by name one-time:
#using (Html.Delayed("one-time", isOnlyOne: "one-time")) {
<b>show me once by name</b>
<span>#Model.First().Value</span>
}
To render:
#Html.RenderDelayed(); // the "default" unidentified blocks
#Html.RenderDelayed("one-time", false); // render the specified block by name, and allow us to render it again in a second call
#Html.RenderDelayed("one-time"); // render the specified block by name
#Html.RenderDelayed("one-time"); // since it was "popped" in the last call, won't render anything
CODE
public static class HtmlRenderExtensions {
/// <summary>
/// Delegate script/resource/etc injection until the end of the page
/// <para>#via https://stackoverflow.com/a/14127332/1037948 and http://jadnb.wordpress.com/2011/02/16/rendering-scripts-from-partial-views-at-the-end-in-mvc/ </para>
/// </summary>
private class DelayedInjectionBlock : IDisposable {
/// <summary>
/// Unique internal storage key
/// </summary>
private const string CACHE_KEY = "DCCF8C78-2E36-4567-B0CF-FE052ACCE309"; // "DelayedInjectionBlocks";
/// <summary>
/// Internal storage identifier for remembering unique/isOnlyOne items
/// </summary>
private const string UNIQUE_IDENTIFIER_KEY = CACHE_KEY;
/// <summary>
/// What to use as internal storage identifier if no identifier provided (since we can't use null as key)
/// </summary>
private const string EMPTY_IDENTIFIER = "";
/// <summary>
/// Retrieve a context-aware list of cached output delegates from the given helper; uses the helper's context rather than singleton HttpContext.Current.Items
/// </summary>
/// <param name="helper">the helper from which we use the context</param>
/// <param name="identifier">optional unique sub-identifier for a given injection block</param>
/// <returns>list of delayed-execution callbacks to render internal content</returns>
public static Queue<string> GetQueue(HtmlHelper helper, string identifier = null) {
return _GetOrSet(helper, new Queue<string>(), identifier ?? EMPTY_IDENTIFIER);
}
/// <summary>
/// Retrieve a context-aware list of cached output delegates from the given helper; uses the helper's context rather than singleton HttpContext.Current.Items
/// </summary>
/// <param name="helper">the helper from which we use the context</param>
/// <param name="defaultValue">the default value to return if the cached item isn't found or isn't the expected type; can also be used to set with an arbitrary value</param>
/// <param name="identifier">optional unique sub-identifier for a given injection block</param>
/// <returns>list of delayed-execution callbacks to render internal content</returns>
private static T _GetOrSet<T>(HtmlHelper helper, T defaultValue, string identifier = EMPTY_IDENTIFIER) where T : class {
var storage = GetStorage(helper);
// return the stored item, or set it if it does not exist
return (T) (storage.ContainsKey(identifier) ? storage[identifier] : (storage[identifier] = defaultValue));
}
/// <summary>
/// Get the storage, but if it doesn't exist or isn't the expected type, then create a new "bucket"
/// </summary>
/// <param name="helper"></param>
/// <returns></returns>
public static Dictionary<string, object> GetStorage(HtmlHelper helper) {
var storage = helper.ViewContext.HttpContext.Items[CACHE_KEY] as Dictionary<string, object>;
if (storage == null) helper.ViewContext.HttpContext.Items[CACHE_KEY] = (storage = new Dictionary<string, object>());
return storage;
}
private readonly HtmlHelper helper;
private readonly string identifier;
private readonly string isOnlyOne;
/// <summary>
/// Create a new using block from the given helper (used for trapping appropriate context)
/// </summary>
/// <param name="helper">the helper from which we use the context</param>
/// <param name="identifier">optional unique identifier to specify one or many injection blocks</param>
/// <param name="isOnlyOne">extra identifier used to ensure that this item is only added once; if provided, content should only appear once in the page (i.e. only the first block called for this identifier is used)</param>
public DelayedInjectionBlock(HtmlHelper helper, string identifier = null, string isOnlyOne = null) {
this.helper = helper;
// start a new writing context
((WebViewPage)this.helper.ViewDataContainer).OutputStack.Push(new StringWriter());
this.identifier = identifier ?? EMPTY_IDENTIFIER;
this.isOnlyOne = isOnlyOne;
}
/// <summary>
/// Append the internal content to the context's cached list of output delegates
/// </summary>
public void Dispose() {
// render the internal content of the injection block helper
// make sure to pop from the stack rather than just render from the Writer
// so it will remove it from regular rendering
var content = ((WebViewPage)this.helper.ViewDataContainer).OutputStack;
var renderedContent = content.Count == 0 ? string.Empty : content.Pop().ToString();
// if we only want one, remove the existing
var queue = GetQueue(this.helper, this.identifier);
// get the index of the existing item from the alternate storage
var existingIdentifiers = _GetOrSet(this.helper, new Dictionary<string, int>(), UNIQUE_IDENTIFIER_KEY);
// only save the result if this isn't meant to be unique, or
// if it's supposed to be unique and we haven't encountered this identifier before
if( null == this.isOnlyOne || !existingIdentifiers.ContainsKey(this.isOnlyOne) ) {
// remove the new writing context we created for this block
// and save the output to the queue for later
queue.Enqueue(renderedContent);
// only remember this if supposed to
if(null != this.isOnlyOne) existingIdentifiers[this.isOnlyOne] = queue.Count; // save the index, so we could remove it directly (if we want to use the last instance of the block rather than the first)
}
}
}
/// <summary>
/// <para>Start a delayed-execution block of output -- this will be rendered/printed on the next call to <see cref="RenderDelayed"/>.</para>
/// <para>
/// <example>
/// Print once in "default block" (usually rendered at end via <code>#Html.RenderDelayed()</code>). Code:
/// <code>
/// #using (Html.Delayed()) {
/// <b>show at later</b>
/// <span>#Model.Name</span>
/// etc
/// }
/// </code>
/// </example>
/// </para>
/// <para>
/// <example>
/// Print once (i.e. if within a looped partial), using identified block via <code>#Html.RenderDelayed("one-time")</code>. Code:
/// <code>
/// #using (Html.Delayed("one-time", isOnlyOne: "one-time")) {
/// <b>show me once</b>
/// <span>#Model.First().Value</span>
/// }
/// </code>
/// </example>
/// </para>
/// </summary>
/// <param name="helper">the helper from which we use the context</param>
/// <param name="injectionBlockId">optional unique identifier to specify one or many injection blocks</param>
/// <param name="isOnlyOne">extra identifier used to ensure that this item is only added once; if provided, content should only appear once in the page (i.e. only the first block called for this identifier is used)</param>
/// <returns>using block to wrap delayed output</returns>
public static IDisposable Delayed(this HtmlHelper helper, string injectionBlockId = null, string isOnlyOne = null) {
return new DelayedInjectionBlock(helper, injectionBlockId, isOnlyOne);
}
/// <summary>
/// Render all queued output blocks injected via <see cref="Delayed"/>.
/// <para>
/// <example>
/// Print all delayed blocks using default identifier (i.e. not provided)
/// <code>
/// #using (Html.Delayed()) {
/// <b>show me later</b>
/// <span>#Model.Name</span>
/// etc
/// }
/// </code>
/// -- then later --
/// <code>
/// #using (Html.Delayed()) {
/// <b>more for later</b>
/// etc
/// }
/// </code>
/// -- then later --
/// <code>
/// #Html.RenderDelayed() // will print both delayed blocks
/// </code>
/// </example>
/// </para>
/// <para>
/// <example>
/// Allow multiple repetitions of rendered blocks, using same <code>#Html.Delayed()...</code> as before. Code:
/// <code>
/// #Html.RenderDelayed(removeAfterRendering: false); /* will print */
/// #Html.RenderDelayed() /* will print again because not removed before */
/// </code>
/// </example>
/// </para>
/// </summary>
/// <param name="helper">the helper from which we use the context</param>
/// <param name="injectionBlockId">optional unique identifier to specify one or many injection blocks</param>
/// <param name="removeAfterRendering">only render this once</param>
/// <returns>rendered output content</returns>
public static MvcHtmlString RenderDelayed(this HtmlHelper helper, string injectionBlockId = null, bool removeAfterRendering = true) {
var stack = DelayedInjectionBlock.GetQueue(helper, injectionBlockId);
if( removeAfterRendering ) {
var sb = new StringBuilder(
#if DEBUG
string.Format("<!-- delayed-block: {0} -->", injectionBlockId)
#endif
);
// .count faster than .any
while (stack.Count > 0) {
sb.AppendLine(stack.Dequeue());
}
return MvcHtmlString.Create(sb.ToString());
}
return MvcHtmlString.Create(
#if DEBUG
string.Format("<!-- delayed-block: {0} -->", injectionBlockId) +
#endif
string.Join(Environment.NewLine, stack));
}
}
Install the Forloop.HtmlHelpers nuget package - it adds some helpers for managing scripts in partial views and editor templates.
Somewhere in your layout, you need to call
#Html.RenderScripts()
This will be where any script files and script blocks will be outputted in the page so I would recommend putting it after your main scripts in the layout and after a scripts section (if you have one).
If you're using The Web Optimization Framework with bundling, you can use the overload
#Html.RenderScripts(Scripts.Render)
so that this method is used for writing out script files.
Now, anytime you want to add script files or blocks in a view, partial view or template, simply use
#using (Html.BeginScriptContext())
{
Html.AddScriptFile("~/Scripts/jquery.validate.js");
Html.AddScriptBlock(
#<script type="text/javascript">
$(function() { $('#someField').datepicker(); });
</script>
);
}
The helpers ensure that only one script file reference is rendered if added multiple times and it also ensures that script files are rendered out in an expected order i.e.
Layout
Partials and Templates (in the order in which they appear in the view, top to bottom)
This post really helped me so I thought I would post my implementation of the basic idea. I've introduced a helper function that can return script tags for use in the #Html.Resource function.
I also added a simple static class so that I can use typed variables to identify a JS or CSS resource.
public static class ResourceType
{
public const string Css = "css";
public const string Js = "js";
}
public static class HtmlExtensions
{
public static IHtmlString Resource(this HtmlHelper htmlHelper, Func<object, dynamic> template, string Type)
{
if (htmlHelper.ViewContext.HttpContext.Items[Type] != null) ((List<Func<object, dynamic>>)htmlHelper.ViewContext.HttpContext.Items[Type]).Add(template);
else htmlHelper.ViewContext.HttpContext.Items[Type] = new List<Func<object, dynamic>>() { template };
return new HtmlString(String.Empty);
}
public static IHtmlString RenderResources(this HtmlHelper htmlHelper, string Type)
{
if (htmlHelper.ViewContext.HttpContext.Items[Type] != null)
{
List<Func<object, dynamic>> resources = (List<Func<object, dynamic>>)htmlHelper.ViewContext.HttpContext.Items[Type];
foreach (var resource in resources)
{
if (resource != null) htmlHelper.ViewContext.Writer.Write(resource(null));
}
}
return new HtmlString(String.Empty);
}
public static Func<object, dynamic> ScriptTag(this HtmlHelper htmlHelper, string url)
{
var urlHelper = new UrlHelper(htmlHelper.ViewContext.RequestContext);
var script = new TagBuilder("script");
script.Attributes["type"] = "text/javascript";
script.Attributes["src"] = urlHelper.Content("~/" + url);
return x => new HtmlString(script.ToString(TagRenderMode.Normal));
}
}
And in use
#Html.Resource(Html.ScriptTag("Areas/Admin/js/plugins/wysiwyg/jquery.wysiwyg.js"), ResourceType.Js)
Thanks to #Darin Dimitrov who supplied the answer in my question here.
The answer given in Populate a Razor Section From a Partial using the RequireScript HtmlHelper follows the same pattern. It also has the benefit that it checks for and suppresses duplicate references to the same Javascript URL, and it has an explicit priority parameter that can be used to control ordering.
I extended this solution by adding methods for:
// use this for scripts to be placed just before the </body> tag
public static string RequireFooterScript(this HtmlHelper html, string path, int priority = 1) { ... }
public static HtmlString EmitRequiredFooterScripts(this HtmlHelper html) { ... }
// use this for CSS links
public static string RequireCSS(this HtmlHelper html, string path, int priority = 1) { ... }
public static HtmlString EmitRequiredCSS(this HtmlHelper html) { ... }
I like Darin's & eth0's solutions though since they use the HelperResult template, which allows for script and CSS blocks, not just links to Javascript and CSS files.
#Darin Dimitrov and #eth0 answers to use with bundle extention usage :
#Html.Resources(a => new HelperResult(b => b.Write( System.Web.Optimization.Scripts.Render("~/Content/js/formBundle").ToString())), "jsTop")