Build events and versioning .js and .css files - msbuild

I have a MSBuild script set up to minify and combine my javascript and css files. What I need now is a way to version them. How are you guys currently handling this. What is the best way to incrementally version the file and update the <script/> tag with the new file name?

I was originally going to suggest using resource expressions to include a version tag from AppSettings, but after testing found that it only works if it is the entire value of a server control property.
The AppSettings value can be updated by the build script for every release; can be any format you like:
<appSettings>
<add key="versionTag" value="27" />
</appSettings>
Works:
<asp:Label runat="server" Text="<%$ AppSettings: versionTag %>" />
Doesn't work:
<link runat="server" rel="Stylesheet" type="text/css"
href='/css/site.css?v=<%$ AppSettings: versionTag %>' />
So my actual recommendation is to create your own controls that read the version tag and include that in their output. Here's how my CSS control looks on a page (NOTE: server controls must be inside the server-side form, even though it may render inside the head element):
...
<form id="form1" runat="server">
<my:Stylesheet runat="server" Url="~/css/site.css" />
</form>
...
The code for my Stylesheet control:
namespace MyNamespace
{
using System;
using System.ComponentModel;
using System.Configuration;
using System.Security.Permissions;
using System.Web;
using System.Web.UI;
using System.Web.UI.HtmlControls;
using System.Web.UI.WebControls;
/// <summary>
/// Outputs a CSS stylesheet link that supports versionable caching via a
/// build-specific query parameter.
/// </summary>
[
AspNetHostingPermission(SecurityAction.InheritanceDemand,
Level = AspNetHostingPermissionLevel.Minimal),
AspNetHostingPermission(SecurityAction.LinkDemand,
Level = AspNetHostingPermissionLevel.Minimal),
DefaultProperty("Href"),
ToolboxData(#"<{0}:Stylesheet runat=""server"" />")
]
public class Stylesheet : WebControl
{
private static string versionTag = Stylesheet.GetVersionTag();
/// <summary>
/// Gets or sets the stylesheet URL.
/// </summary>
public string Href
{
get
{
return this.ViewState["Href"] as string;
}
set
{
this.ViewState["Href"] = value;
}
}
/// <summary>
/// Raises the PreRender event.
/// </summary>
/// <param name="e">Contains the event data.</param>
protected override void OnPreRender(EventArgs e)
{
base.OnPreRender(e);
HtmlLink link = new HtmlLink();
link.Href = String.Format(
"{0}?v={1}",
this.Page.ResolveUrl(this.Href),
HttpUtility.UrlEncode(Stylesheet.versionTag));
if (!Stylesheet.HeadContainsLinkHref(this.Page, link.Href))
{
link.Attributes["type"] = "text/css";
link.Attributes["rel"] = "Stylesheet";
this.Page.Header.Controls.Add(link);
}
}
/// <summary>
/// Generates content to be rendered on the client.
/// </summary>
/// <param name="writer">Receives the server control content.</param>
protected override void Render(HtmlTextWriter writer)
{
// Do nothing.
}
/// <summary>
/// Retrieves the script version tag for this build.
/// </summary>
/// <returns>Returns the script version tag.</returns>
private static string GetVersionTag()
{
string tag = ConfigurationManager.AppSettings["versionTag"];
if (String.IsNullOrEmpty(tag))
{
tag = "1";
}
return tag;
}
/// <summary>
/// Determines if the page's <c>head</c> contains a <c>link</c> tag
/// with a matching <c>href</c> attribute value.
/// </summary>
/// <param name="thePage">The Page to be tested.</param>
/// <param name="href">The <c>href</c> URL to be matched.</param>
/// <returns>Returns true if a matching link is already part of the
/// page <c>head</c> or false otherwise.</returns>
public static bool HeadContainsLinkHref(Page thePage, string href)
{
if (thePage == null)
{
throw new ArgumentNullException("thePage");
}
foreach (Control control in thePage.Header.Controls)
{
if ((control is HtmlLink) &&
(control as HtmlLink).Href == href)
{
return true;
}
}
return false;
}
}
}
HTH.

Related

Auto detect URL, phone number, email in TextBlock

I would like to automatically highlight URL, Email and phone number in UWP. It is possible in Android but it seems this features has been forgotten by Microsoft.
In my use case, I get the text from a web service, so I don't know the text format which is a user text input on the web platform.
The platform is not supporting this feature (yet). When I've to do the same thing, I've ended with my own solution which is to:
create an attached property receiving the text to format
use some regex to extract the URL, phone numbers, email addresses from it
generate an Inlines collection which I'm injecting to the attached TextBlock control
The regex used are covering a lot of cases but some edge cases can be still missing.
It is used this way:
<TextBlock uwpext:TextBlock.InteractiveText="Here is a link www.bing.com to send to a#a.com or 0000000000" />
The attached property code:
// -------------------------------------------------------------------------------------------
/// <summary>
/// The regex to detect the URL from the text content
/// It comes from https://gist.github.com/gruber/249502 (http://daringfireball.net/2010/07/improved_regex_for_matching_urls)
/// </summary>
private static readonly Regex UrlRegex = new Regex(#"(?i)\b((?:[a-z][\w-]+:(?:/{1,3}|[a-z0-9%])|www\d{0,3}[.]|[a-z0-9.\-]+[.][a-z]{2,4}/)(?:[^\s()<>]+|\(([^\s()<>]+|(\([^\s()<>]+\)))*\))+(?:\(([^\s()<>]+|(\([^\s()<>]+\)))*\)|[^\s`!()\[\]{};:'"".,<>?«»“”‘’]))", RegexOptions.IgnoreCase, TimeSpan.FromMilliseconds(500));
// -------------------------------------------------------------------------------------------
/// <summary>
/// The regex to detect the email addresses
/// It comes from https://msdn.microsoft.com/en-us/library/01escwtf.aspx
/// </summary>
private static readonly Regex EmailRegex = new Regex(#"(?("")("".+?(?<!\\)""#)|(([0-9a-z]((\.(?!\.))|[-!#\$%&'\*\+/=\?\^`\{\}\|~\w])*)(?<=[0-9a-z])#))(?(\[)(\[(\d{1,3}\.){3}\d{1,3}\])|(([0-9a-z][-\w]*[0-9a-z]*\.)+[a-z0-9][\-a-z0-9]{0,22}[a-z0-9]))", RegexOptions.IgnoreCase, TimeSpan.FromMilliseconds(500));
// -------------------------------------------------------------------------------------------
/// <summary>
/// The regex to detect the phone numbers from the raw message
/// </summary>
private static readonly Regex PhoneRegex = new Regex(#"\+?[\d\-\(\)\. ]{5,}", RegexOptions.IgnoreCase, TimeSpan.FromMilliseconds(250));
// -------------------------------------------------------------------------------------------
/// <summary>
/// The default prefix to use to convert a relative URI to an absolute URI
/// The Windows RunTime is only working with absolute URI
/// </summary>
private const string RelativeUriDefaultPrefix = "http://";
// -------------------------------------------------------------------------------------------
/// <summary>
/// The dependency property to generate an interactive text in a text block.
/// When setting this property, we will parse the value and transform the hyperlink or the email address to interactive fields that the user can interact width.
/// The raw text will be parsed and convert to a collection of inlines.
/// </summary>
public static readonly DependencyProperty InteractiveTextProperty = DependencyProperty.RegisterAttached("InteractiveText", typeof(string), typeof(TextBlock), new PropertyMetadata(null, OnInteractiveTextChanged));
// -------------------------------------------------------------------------------------------
/// <summary>
/// The event callback for the interactive text changed event
/// We will parse the raw text and generate the inlines that will wrap the interactive items (URL...)
/// </summary>
/// <param name="d">the object which has raised the event</param>
/// <param name="e">the change information</param>
private static void OnInteractiveTextChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
var textBlock = d as Windows.UI.Xaml.Controls.TextBlock;
if(textBlock == null) return;
// we remove all the inlines
textBlock.Inlines.Clear();
// if we have no data, we do not need to go further
var rawText = e.NewValue as string;
if(string.IsNullOrEmpty(rawText)) return;
var lastPosition = 0;
var matches = new Match[3];
do
{
matches[0] = UrlRegex.Match(rawText, lastPosition);
matches[1] = EmailRegex.Match(rawText, lastPosition);
matches[2] = PhoneRegex.Match(rawText, lastPosition);
var firstMatch = matches.Where(x => x.Success).OrderBy(x => x.Index).FirstOrDefault();
if(firstMatch == matches[0])
{
// the first match is an URL
CreateRunElement(textBlock, rawText, lastPosition, firstMatch.Index);
lastPosition = CreateUrlElement(textBlock, firstMatch);
}
else if(firstMatch == matches[1])
{
// the first match is an email
CreateRunElement(textBlock, rawText, lastPosition, firstMatch.Index);
lastPosition = CreateContactElement(textBlock, firstMatch, null);
}
else if(firstMatch == matches[2])
{
// the first match is a phonenumber
CreateRunElement(textBlock, rawText, lastPosition, firstMatch.Index);
lastPosition = CreateContactElement(textBlock, null, firstMatch);
}
else
{
// no match, we add the whole text
textBlock.Inlines.Add(new Run { Text = rawText.Substring(lastPosition) });
lastPosition = rawText.Length;
}
}
while(lastPosition < rawText.Length);
}
// -------------------------------------------------------------------------------------------
/// <summary>
/// This method will extract a fragment of the raw text string, create a Run element with the fragment and
/// add it to the textblock inlines collection
/// </summary>
/// <param name="textBlock">the textblock where to add the run element</param>
/// <param name="rawText">the raw text where the fragment will be extracted</param>
/// <param name="startPosition">the start position to extract the fragment</param>
/// <param name="endPosition">the end position to extract the fragment</param>
private static void CreateRunElement(Windows.UI.Xaml.Controls.TextBlock textBlock, string rawText, int startPosition, int endPosition)
{
var fragment = rawText.Substring(startPosition, endPosition - startPosition);
textBlock.Inlines.Add(new Run { Text = fragment });
}
// -------------------------------------------------------------------------------------------
/// <summary>
/// Create an URL element with the provided match result from the URL regex
/// It will create the Hyperlink element that will contain the URL and add it to the provided textblock
/// </summary>
/// <param name="textBlock">the textblock where to add the hyperlink</param>
/// <param name="urlMatch">the match for the URL to use to create the hyperlink element</param>
/// <returns>the newest position on the source string for the parsing</returns>
private static int CreateUrlElement(Windows.UI.Xaml.Controls.TextBlock textBlock, Match urlMatch)
{
Uri targetUri;
if(Uri.TryCreate(urlMatch.Value, UriKind.RelativeOrAbsolute, out targetUri))
{
var link = new Hyperlink();
link.Inlines.Add(new Run { Text= urlMatch.Value });
if(targetUri.IsAbsoluteUri)
link.NavigateUri = targetUri;
else
link.NavigateUri = new Uri(RelativeUriDefaultPrefix + targetUri.OriginalString);
textBlock.Inlines.Add(link);
}
else
{
textBlock.Inlines.Add(new Run { Text= urlMatch.Value });
}
return urlMatch.Index + urlMatch.Length;
}
// -------------------------------------------------------------------------------------------
/// <summary>
/// Create a hyperlink element with the provided match result from the regex that will open the contact application
/// with the provided contact information (it should be a phone number or an email address
/// This is used only if the email address / phone number is not prefixed with the mailto: / tel: scheme
/// It will create the Hyperlink element that will contain the email/phone number hyperlink and add it to the provided textblock.
/// Clicking on the link will open the contact application
/// </summary>
/// <param name="textBlock">the textblock where to add the hyperlink</param>
/// <param name="emailMatch">the match for the email to use to create the hyperlink element. Set to null if not available but at least one of emailMatch and phoneMatch must be not null.</param>
/// <param name="phoneMatch">the match for the phone number to create the hyperlink element. Set to null if not available but at least one of emailMatch and phoneMatch must be not null.</param>
/// <returns>the newest position on the source string for the parsing</returns>
private static int CreateContactElement(Windows.UI.Xaml.Controls.TextBlock textBlock, Match emailMatch, Match phoneMatch)
{
var currentMatch = emailMatch ?? phoneMatch;
var link = new Hyperlink();
link.Inlines.Add(new Run { Text= currentMatch.Value });
link.Click += (s, a) =>
{
var contact = new Contact();
if(emailMatch != null) contact.Emails.Add(new ContactEmail { Address = emailMatch.Value });
if(phoneMatch != null) contact.Phones.Add(new ContactPhone { Number = phoneMatch.Value.StripNonDigitsCharacters() });
ContactManager.ShowFullContactCard(contact, new FullContactCardOptions());
};
textBlock.Inlines.Add(link);
return currentMatch.Index + currentMatch.Length;
}
// -------------------------------------------------------------------------------------------
/// <summary>
/// Return the InteractiveText value on the provided object
/// </summary>
/// <param name="obj">the object to query</param>
/// <returns>the InteractiveText value</returns>
public static string GetInteractiveText(DependencyObject obj)
{
return (string) obj.GetValue(InteractiveTextProperty);
}
// -------------------------------------------------------------------------------------------
/// <summary>
/// SEt the InteractiveText value on the provided object
/// </summary>
/// <param name="obj">the object to query</param>
/// <param name="value">the value to set</param>
public static void SetInteractiveText(DependencyObject obj, string value)
{
obj.SetValue(InteractiveTextProperty, value);
}

MVC Custom ClientSide Validation

I have the following custom required attribute code:
[AttributeUsage(AttributeTargets.Property, AllowMultiple = true, Inherited = true)]
public sealed class AddressRequiredAttribute : RequiredAttribute, IClientValidatable
{
/// <summary>
/// The _property name
/// </summary>
private string _propertyName;
/// <summary>
/// Initializes a new instance of the <see cref="AddressRequiredAttribute"/> class.
/// </summary>
/// <param name="propertyName">Name of the property.</param>
public AddressRequiredAttribute(string propertyName)
: base()
{
_propertyName = propertyName;
}
/// <summary>
/// Checks that the value of the required data field is not empty.
/// </summary>
/// <param name="value">The data field value to validate.</param>
/// <returns>
/// true if validation is successful; otherwise, false.
/// </returns>
protected override ValidationResult IsValid(object value, ValidationContext context)
{
if (context.ObjectType.BaseType == typeof(AddressModel))
{
PropertyInfo property = context.ObjectType.GetProperty(_propertyName);
if (property != null && (bool)property.GetValue(context.ObjectInstance))
{
return base.IsValid(value, context);
}
}
return ValidationResult.Success;
}
/// <summary>
/// When implemented in a class, returns client validation rules for that class.
/// </summary>
/// <param name="metadata">The model metadata.</param>
/// <param name="context">The controller context.</param>
/// <returns>
/// The client validation rules for this validator.
/// </returns>
public IEnumerable<ModelClientValidationRule> GetClientValidationRules(ModelMetadata metadata, ControllerContext context)
{
string errorMessage = this.ErrorMessage;
// Get the specific error message if set, otherwise the default
if (string.IsNullOrEmpty(errorMessage) && metadata != null)
{
errorMessage = FormatErrorMessage(metadata.GetDisplayName());
}
var clientValidationRule = new ModelClientValidationRule()
{
ErrorMessage = errorMessage,
ValidationType = "requiredaddress"
};
return new[] { clientValidationRule };
}
and the following jquery for the client side validation which is run on window.load:
$.validator.addMethod('requiredaddress', function (value, element, params) {
return value != '';
}, '');
$.validator.unobtrusive.adapters.add('requiredaddress', {}, function (options) {
options.rules['requiredaddress'] = true;
options.messages['requiredaddress'] = options.message;
});
However, the clientside doesn't kick in so I get the normal clientside validation working, then the form will submit but come back with the custom errors after postback. All the examples that I have looked at say that my code should be correct so I'm not sure what is wrong here.
Can anyone see anything obvious I'm doing wrong here, thanks
I did something similar to this and found that if I put the code in the document.ready on window.load it wouldn't work. In the end I went with using the following code placed after the jquery validate scripts:
(function ($) {
$.validator.addMethod('requiredaddress', function (value, element, params) {
return value != '';
}, 'Clientside Should Not Postback');
// i think you should be able to use this as your adapter
$.validator.unobtrusive.adapters.addBool('requiredaddress');
})(jQuery);

How to use ASP.Net MVC 4 to Bundle LESS files in Release mode?

I'm trying to have LESS files in my web project, and have the MVC 4 bundling functionality call into the dotLess library to turn the LESS into CSS, then minify the result and give it to the browser.
I found an example on the ASP.NET site (under the heading LESS, CoffeeScript, SCSS, Sass Bundling.). This has given me a LessTransform class that looks like this:
public class LessTransform : IBundleTransform
{
public void Process(BundleContext context, BundleResponse response)
{
response.Content = dotless.Core.Less.Parse(response.Content);
response.ContentType = "text/css";
}
}
and this line in my BundleConfig class:
bundles.Add(new Bundle(
"~/Content/lessTest",
new LessTransform(),
new CssMinify()).Include("~/Content/less/test.less"));
finally I have the following line in my _Layout.cshtml, in the <head>:
#Styles.Render("~/Content/lessTest")
If I have the site in debug mode, this is rendered to the browser:
<link href="/Content/less/test.less" rel="stylesheet"/>
The rules in the .less file are applied, and following that link shows that the LESS has been correctly transformed into CSS.
However, if I put the site into release mode, this is rendered out:
<link href="/Content/less?v=lEs-HID6XUz3s2qkJ35Lvnwwq677wTaIiry6fuX8gz01" rel="stylesheet"/>
The rules in the .less file are not applied, because following the link gives a 404 error from IIS.
So it seems that something is going wrong with the bundling. How do I get this to work in release mode, or how do I find out what exactly is going wrong?
As a complement to the accepted answer, I created a LessBundle class, which is the Less eqivalent of the StyleBundle class.
LessBundle.cs code is:
using System.Web.Optimization;
namespace MyProject
{
public class LessBundle : Bundle
{
public LessBundle(string virtualPath) : base(virtualPath, new IBundleTransform[] {new LessTransform(), new CssMinify()})
{
}
public LessBundle(string virtualPath, string cdnPath)
: base(virtualPath, cdnPath, new IBundleTransform[] { new LessTransform(), new CssMinify() })
{
}
}
}
Usage is similar to the StyleBundle class, specifying a LESS file instead of a CSS file.
Add the following to your BundleConfig.RegisterBundles(BundleCollection) method:
bundles.Add(new LessBundle("~/Content/less").Include(
"~/Content/MyStyles.less"));
Update
This method works fine with optimization switched off, but I ran into some minor problems (with CSS resource paths) when optimization was switched on. After an hour researching the issue I discovered that I have reinvented the wheel...
If you do want the LessBundle functionality I describe above, check out System.Web.Optimization.Less.
The NuGet package can be found here.
Edited 12/8/2019 This is no longer an acceptable answer to this issue as there have been breaking changes in ASP.NET over the years. There are other answers further down that have modified this code or supplied other answers to help you fix this issue.
It appears that the dotless engine needs to know the path of the currently processed bundle file to resolve #import paths. If you run the process code that you have above, the result of the dotless.Core.Less.Parse() is an empty string when the .less file being parsed has other less files imported.
Ben Foster's response here will fix that by reading the imported files first:
Import Files and DotLess
Change your LessTransform file as follows:
public class LessTransform : IBundleTransform
{
public void Process(BundleContext context, BundleResponse bundle)
{
if (context == null)
{
throw new ArgumentNullException("context");
}
if (bundle == null)
{
throw new ArgumentNullException("bundle");
}
context.HttpContext.Response.Cache.SetLastModifiedFromFileDependencies();
var lessParser = new Parser();
ILessEngine lessEngine = CreateLessEngine(lessParser);
var content = new StringBuilder(bundle.Content.Length);
var bundleFiles = new List<FileInfo>();
foreach (var bundleFile in bundle.Files)
{
bundleFiles.Add(bundleFile);
SetCurrentFilePath(lessParser, bundleFile.FullName);
string source = File.ReadAllText(bundleFile.FullName);
content.Append(lessEngine.TransformToCss(source, bundleFile.FullName));
content.AppendLine();
bundleFiles.AddRange(GetFileDependencies(lessParser));
}
if (BundleTable.EnableOptimizations)
{
// include imports in bundle files to register cache dependencies
bundle.Files = bundleFiles.Distinct();
}
bundle.ContentType = "text/css";
bundle.Content = content.ToString();
}
/// <summary>
/// Creates an instance of LESS engine.
/// </summary>
/// <param name="lessParser">The LESS parser.</param>
private ILessEngine CreateLessEngine(Parser lessParser)
{
var logger = new AspNetTraceLogger(LogLevel.Debug, new Http());
return new LessEngine(lessParser, logger, true, false);
}
/// <summary>
/// Gets the file dependencies (#imports) of the LESS file being parsed.
/// </summary>
/// <param name="lessParser">The LESS parser.</param>
/// <returns>An array of file references to the dependent file references.</returns>
private IEnumerable<FileInfo> GetFileDependencies(Parser lessParser)
{
IPathResolver pathResolver = GetPathResolver(lessParser);
foreach (var importPath in lessParser.Importer.Imports)
{
yield return new FileInfo(pathResolver.GetFullPath(importPath));
}
lessParser.Importer.Imports.Clear();
}
/// <summary>
/// Returns an <see cref="IPathResolver"/> instance used by the specified LESS lessParser.
/// </summary>
/// <param name="lessParser">The LESS parser.</param>
private IPathResolver GetPathResolver(Parser lessParser)
{
var importer = lessParser.Importer as Importer;
var fileReader = importer.FileReader as FileReader;
return fileReader.PathResolver;
}
/// <summary>
/// Informs the LESS parser about the path to the currently processed file.
/// This is done by using a custom <see cref="IPathResolver"/> implementation.
/// </summary>
/// <param name="lessParser">The LESS parser.</param>
/// <param name="currentFilePath">The path to the currently processed file.</param>
private void SetCurrentFilePath(Parser lessParser, string currentFilePath)
{
var importer = lessParser.Importer as Importer;
if (importer == null)
throw new InvalidOperationException("Unexpected dotless importer type.");
var fileReader = importer.FileReader as FileReader;
if (fileReader == null || !(fileReader.PathResolver is ImportedFilePathResolver))
{
fileReader = new FileReader(new ImportedFilePathResolver(currentFilePath));
importer.FileReader = fileReader;
}
}
}
public class ImportedFilePathResolver : IPathResolver
{
private string currentFileDirectory;
private string currentFilePath;
public ImportedFilePathResolver(string currentFilePath)
{
if (string.IsNullOrEmpty(currentFilePath))
{
throw new ArgumentNullException("currentFilePath");
}
CurrentFilePath = currentFilePath;
}
/// <summary>
/// Gets or sets the path to the currently processed file.
/// </summary>
public string CurrentFilePath
{
get { return currentFilePath; }
set
{
currentFilePath = value;
currentFileDirectory = Path.GetDirectoryName(value);
}
}
/// <summary>
/// Returns the absolute path for the specified improted file path.
/// </summary>
/// <param name="filePath">The imported file path.</param>
public string GetFullPath(string filePath)
{
if (filePath.StartsWith("~"))
{
filePath = VirtualPathUtility.ToAbsolute(filePath);
}
if (filePath.StartsWith("/"))
{
filePath = HostingEnvironment.MapPath(filePath);
}
else if (!Path.IsPathRooted(filePath))
{
filePath = Path.GetFullPath(Path.Combine(currentFileDirectory, filePath));
}
return filePath;
}
}
The accepted answer does not work with recent changes to ASP.NET, so is no longer correct.
I've fixed the source in the accepted answer:
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text;
using System.Web.Hosting;
using System.Web.Optimization;
using dotless.Core;
using dotless.Core.Abstractions;
using dotless.Core.Importers;
using dotless.Core.Input;
using dotless.Core.Loggers;
using dotless.Core.Parser;
namespace Web.App_Start.Bundles
{
public class LessTransform : IBundleTransform
{
public void Process(BundleContext context, BundleResponse bundle)
{
if (context == null)
{
throw new ArgumentNullException("context");
}
if (bundle == null)
{
throw new ArgumentNullException("bundle");
}
context.HttpContext.Response.Cache.SetLastModifiedFromFileDependencies();
var lessParser = new Parser();
ILessEngine lessEngine = CreateLessEngine(lessParser);
var content = new StringBuilder(bundle.Content.Length);
var bundleFiles = new List<BundleFile>();
foreach (var bundleFile in bundle.Files)
{
bundleFiles.Add(bundleFile);
var name = context.HttpContext.Server.MapPath(bundleFile.VirtualFile.VirtualPath);
SetCurrentFilePath(lessParser, name);
using (var stream = bundleFile.VirtualFile.Open())
using (var reader = new StreamReader(stream))
{
string source = reader.ReadToEnd();
content.Append(lessEngine.TransformToCss(source, name));
content.AppendLine();
}
bundleFiles.AddRange(GetFileDependencies(lessParser));
}
if (BundleTable.EnableOptimizations)
{
// include imports in bundle files to register cache dependencies
bundle.Files = bundleFiles.Distinct();
}
bundle.ContentType = "text/css";
bundle.Content = content.ToString();
}
/// <summary>
/// Creates an instance of LESS engine.
/// </summary>
/// <param name="lessParser">The LESS parser.</param>
private ILessEngine CreateLessEngine(Parser lessParser)
{
var logger = new AspNetTraceLogger(LogLevel.Debug, new Http());
return new LessEngine(lessParser, logger, true, false);
}
/// <summary>
/// Gets the file dependencies (#imports) of the LESS file being parsed.
/// </summary>
/// <param name="lessParser">The LESS parser.</param>
/// <returns>An array of file references to the dependent file references.</returns>
private IEnumerable<BundleFile> GetFileDependencies(Parser lessParser)
{
IPathResolver pathResolver = GetPathResolver(lessParser);
foreach (var importPath in lessParser.Importer.Imports)
{
yield return
new BundleFile(pathResolver.GetFullPath(importPath),
HostingEnvironment.VirtualPathProvider.GetFile(importPath));
}
lessParser.Importer.Imports.Clear();
}
/// <summary>
/// Returns an <see cref="IPathResolver"/> instance used by the specified LESS lessParser.
/// </summary>
/// <param name="lessParser">The LESS parser.</param>
private IPathResolver GetPathResolver(Parser lessParser)
{
var importer = lessParser.Importer as Importer;
var fileReader = importer.FileReader as FileReader;
return fileReader.PathResolver;
}
/// <summary>
/// Informs the LESS parser about the path to the currently processed file.
/// This is done by using a custom <see cref="IPathResolver"/> implementation.
/// </summary>
/// <param name="lessParser">The LESS parser.</param>
/// <param name="currentFilePath">The path to the currently processed file.</param>
private void SetCurrentFilePath(Parser lessParser, string currentFilePath)
{
var importer = lessParser.Importer as Importer;
if (importer == null)
throw new InvalidOperationException("Unexpected dotless importer type.");
var fileReader = importer.FileReader as FileReader;
if (fileReader == null || !(fileReader.PathResolver is ImportedFilePathResolver))
{
fileReader = new FileReader(new ImportedFilePathResolver(currentFilePath));
importer.FileReader = fileReader;
}
}
}
}
Please note one known issue with this code as is is that LESS #imports must use their full paths, i.e. you must use #import "~/Areas/Admin/Css/global.less"; instead of #import "global.less";.
Looks like this works - I changed the Process method to iterate over the file collection:
public void Process(BundleContext context, BundleResponse response)
{
var builder = new StringBuilder();
foreach (var fileInfo in response.Files)
{
using (var reader = fileInfo.OpenText())
{
builder.Append(dotless.Core.Less.Parse(reader.ReadToEnd()));
}
}
response.Content = builder.ToString();
response.ContentType = "text/css";
}
This breaks if there are any #import statements in your less files though, in this case you have to do a bit more work, like this: https://gist.github.com/chrisortman/2002958
Already some great answers, here's a very simple solution I found for myself when trying to add MVC bundles that regard less files.
After creating your less file (for example, test.less), right click on it and under Web Compiler (get it here) option, select Compile File.
This generates the resulting css file from your less one, and also its minified version. (test.css and test.min.css).
On your bundle, just refer to the generated css file
style = new StyleBundle("~/bundles/myLess-styles")
.Include("~/Content/css/test.css", new CssRewriteUrlTransform());
bundles.Add(style);
And on your view, reference that bundle:
#Styles.Render("~/bundles/myLess-styles")
It should just work fine.

SignalR + Uncaught TypeError: Object #<Object> has no method 'sending'

I was previously using an older ver of the signalR.js and everything is fine except it is intermittently causing my page to hang therefore I want to test it out with a newer ver from downloaded from the SignalR github site.
I tried following the client side example of SignalR but I get this error when inspecting element in chrome,
Uncaught TypeError: Object # has no method 'sending'. Anyone came across this error?
<script type="text/javascript" src="https://ajax.googleapis.com/ajax/libs/jquery/1.7.2/jquery.min.js"> </script>
<script src="/Scripts/jquery.signalR.js" type="text/javascript"> </script>
<script src="signalr/hubs" type="text/javascript"> </script>
var hub = $.connection.testHub;
hub.showMessage = function() {
$("#inboxcount").show();
};
$.connection.hub.start()
.done(function() {
hub.subscribe($('#<%= hdnUserId.ClientID %>').val());
})
.fail(function() {
alert("Could not Connect!");
});
TestHub.cs:
using System.Threading;
{
/// <summary>
/// Test Hub used to demonstrate the key concepts of SignalR
/// </summary>
[SignalR.Hubs.HubName("testHub")]
public class TestHub : SignalR.Hubs.Hub
{
/// <summary>
/// Broadcast the message to all clients
/// </summary>
/// <param name="message">message to be broadcasted</param>
public void Broadcast(string message)
{
this.Clients.showMessage(message);
}
/// <summary>
/// Return a string with the formate, Hello [current user name]
/// </summary>
/// <returns></returns>
public string SayHello()
{
//Context property can be used to retreive HTTP attributes like User
return "Hello " + Context.User.Identity.Name;
}
/// <summary>
/// Simulates a long running process that updates its progress
/// </summary>
public void LongRunningMethod()
{
Thread.Sleep(1000);
this.Caller.showMessage("25% Completed");
Thread.Sleep(1000);
this.Caller.showMessage("50% Completed");
Thread.Sleep(1000);
this.Caller.showMessage("75% Completed");
Thread.Sleep(1000);
this.Caller.showMessage("Done");
}
/// <summary>
/// Subscribe to a given message category
/// </summary>
/// <param name="category">the category to subscribe</param>
public void Subscribe(string category)
{
//Add current connection to a connection group with the name 'category'
this.AddToGroup(category);
}
/// <summary>
/// Publish a message to the given mmessage category
/// </summary>
/// <param name="category">the category to send the message</param>
/// <param name="message">the message to be sent</param>
public void Publish(string category, string message)
{
//Broadcast the message to all connections registered under the group 'category'
this.Clients[category].showMessage(message);
}
}
You should probably use
<script src="#Url.Content("~/signalr/hubs")" type="text/javascript"> </script>
instead of
<script src="signalr/hubs" type="text/javascript"> </script>
If that doesn't help, you should check if you don't have any old SignalR libraries in your project: So remove every SignalR dll, and add:
Microsoft.AspNet.SignalR.Core
Microsoft.AspNet.SignalR.Hosting.AspNet
Microsoft.AspNet.SignalR.Hosting.Common
That did the trick for me :)
Add server or client in hub call
hub.server.subscribe

How to use WixSharp to install a website and associate an AppPool

I am trying to find examples of how to use WixSharp (managed code interface to WiX) to install a website and associate an AppPool.
The steps I want to achieve are:
If the website exists in IIS 6, delete it.
If the AppPool exists in IIS 6, delete it.
Delete the application artifacts from the destination directory.
Copy the new application artifacts to the destination directory.
Create the AppPool.
Create the Website, linking it to the AppPool.
I have achieved this in MSBuild but that is not as useful as an MSI. Hence I am trying to "rewrite" the above in WixSharp syntax.
WixSharp apparently supports WIXIISExtension but Google has not yielded any examples yet.
How would I code the above in WixSharp?
I am using WIX for the same purpose. The application I am trying to deploy is around 300 MB and I need to create virtual directory for same, app pool, etc.
I think your requirement is same.
I would suggest WIX is really good for this. You can have screens asking user virtual directory name, Application Pool, etc.
WIX code works perfectly for IIS 5.1, 6, 7. For 7.5 you need to create a customaction. As such you can use wix to create virtual directory even for IIS 7.5 if IIS 6 compatibility mode is installed.
Uptill now I haven't faced any errors using WIX to deploy web applications.
Good question.
I am using WixSharp for my current project and I am really happy with it. It is awesome how you can avoid writing XML files by doing all with C# syntax. By the way, I don't think you are reinventing the wheel... you are speeding up the wheel with WixSharp.
As WixSharp version 1.9.6 doesn't provide creating a WebSite with an associated WebAppPool, I did it by creating a CustomWebSite.cs file. In this way, I created my Web Application using this code:
...
var project = new ManagedProject("My Project",
new InstallDir(#"c:\my_tool",
new Dir("my_frontend",
new Files($"{frontendDir}\\app\\*.*"),
new CustomWebSite("GateKeeper", "*:31515")
{
WebApplication = new CustomWebApplication("DemoApp")
{
WebAppPool = new WebAppPool("DemoApp", "ManagedPipelineMode=Integrated;Identity=applicationPoolIdentity"),
},
InstallWebSite = true
}
)
),
...
Here is my CustomWebSite.cs file that I only used to create one WebSite and I am sure it could be better:
using System;
using System.Collections.Generic;
using System.Xml.Linq;
using WixSharp;
using WixSharp.CommonTasks;
using static WixSharp.WebSite;
namespace ToolBox.WixSharp
{
/// <summary>
/// Defines the WebSite element to be created associated to a Dir element.
/// </summary>
///<example>The following is an example of associating a CustomWebSite to a Dir element.
///
///<code>
/// var project =
/// new Project("My Product",
/// new Dir(#"%ProgramFiles%\My Company\My Product",
/// new Dir(#"some_dir",
/// new CustomWebSite("MyApp", "*:81")
/// {
/// WebApplication = new CustomWebApplication("DemoApp")
/// {
/// WebAppPool = new WebAppPool("DemoApp", "ManagedPipelineMode=Integrated;Identity=applicationPoolIdentity")
/// }
/// InstallWebSite = true
/// }
/// ...
///
/// Compiler.BuildMsi(project);
///</code>
///
/// This code will generate something like this:
///<code>
/// <Component Id="DemoApp_WebSite" Guid="a6896bba-1818-43e0-824f-9c585b3e366b" KeyPath="yes" Win64="yes">
/// <iis:WebSite Id = "DemoApp_WebSite" Description="DemoApp_WebSite" Directory="INSTALLDIR.some_dir">
/// <iis:WebAddress Id = "WebSite_Address1" IP="*" Port="31515" />
/// <iis:WebApplication Id = "DemoApp_WebApplication" Name="DemoApp" WebAppPool="DemoApp_AppPool"/>
/// </iis:WebSite>
/// <iis:WebAppPool Id = "DemoApp_AppPool" Name="DemoApp" ManagedPipelineMode="Integrated" Identity="applicationPoolIdentity" />
///
/// <CreateFolder />
/// <RemoveFolder Id = "INSTALLDIR.some_dir" On="uninstall" />
/// </Component>
/// </code>
/// </example>
public class CustomWebSite : WixEntity, IGenericEntity
{
/// <summary>
/// Indicates if the WebSite is to be installed (created on IIS) or existing WebSite should be used to install the corresponding
/// WebApplication. The default <see cref="InstallWebSite"/> value is <c>false</c>
/// <para>Developers should be aware of the WebSite installation model imposed by WiX/MSI and use <see cref="InstallWebSite"/> carefully.</para>
/// <para>If <see cref="InstallWebSite"/> value is set to <c>false</c> the parent WebApplication (<see cref="T:WixSharp.IISVirtualDir"/>)
/// will be installed in the brand new (freshly created) WebSite or in the existing one if a site with the same address/port combination already exists
/// on IIS). The undesirable side affect of this deployment scenario is that if the existing WebSite was used to install the WebApplication it will be
/// deleted on IIS during uninstallation even if this WebSite has other WebApplications installed.</para>
/// <para>The "safer" option is to set <see cref="InstallWebSite"/> value to <c>true</c> (default value). In this case the WebApplication will
/// be installed in an existing WebSite with matching address/port. If the match is not found the installation will fail. During the uninstallation
/// only installed WebApplication will be removed from IIS.</para>
/// </summary>
public bool InstallWebSite = false;
/// <summary>
/// Initializes a new instance of the <see cref="WebSite" /> class.
/// </summary>
public CustomWebSite()
{
}
/// <summary>
/// Initializes a new instance of the <see cref="CustomWebSite"/> class.
/// </summary>
/// <param name="description">The description of the web site (as it shows up in the IIS manager console).</param>
/// <param name="addressDefinition">The address definition.</param>
public CustomWebSite(string description, string addressDefinition)
{
this.Id = $"{description}_WebSite";
this.Description = description;
this.AddressesDefinition = addressDefinition;
}
/// <summary>
/// Initializes a new instance of the <see cref="CustomWebSite"/> class.
/// </summary>
/// <param name="id">The id</param>
/// <param name="description">The description of the web site (as it shows up in the IIS manager console).</param>
/// <param name="addressDefinition">The address definition.</param>
public CustomWebSite(Id id, string description, string addressDefinition)
{
this.Id = id;
this.Description = description;
this.AddressesDefinition = addressDefinition;
}
internal void ProcessAddressesDefinition()
{
if (!AddressesDefinition.IsEmpty())
{
List<WebAddress> addressesToAdd = new List<WebAddress>();
foreach (string addressDef in AddressesDefinition.Split(";".ToCharArray(), StringSplitOptions.RemoveEmptyEntries))
{
try
{
string[] tokens = addressDef.Split(":".ToCharArray(), StringSplitOptions.RemoveEmptyEntries);
string address = tokens[0];
string port = tokens[1];
if (tokens[1].ContainsWixConstants())
{
addressesToAdd.Add(new WebAddress { Address = address, AttributesDefinition = "Port=" + port });
}
else
{
addressesToAdd.Add(new WebAddress { Address = address, Port = Convert.ToInt32(port) });
}
}
catch (Exception e)
{
throw new Exception("Invalid AddressesDefinition", e);
}
}
this.addresses = addressesToAdd.ToArray();
}
}
/// <summary>
/// References a WebAppPool instance to use as the application pool for this application in IIS 6 applications.
/// </summary>
public string WebAppPool; //WebApplication element attribute
/// <summary>
/// Specification for auto-generating the <see cref="T:WebSite.WebAddresses"/> collection.
/// <para>If <see cref="AddressesDefinition"/> is specified, the existing content of <see cref="Addresses"/> will be ignored
/// and replaced with the auto-generated one at compile time.</para>
/// </summary>
/// <example>
/// <c>webSite.AddressesDefinition = "*:80;*90";</c> will be parsed and converted to an array of <see cref="T:WixSharp.WebSite.WebAddress"/> as follows:
/// <code>
/// ...
/// webSite.Addresses = new []
/// {
/// new WebSite.WebAddress
/// {
/// Address = "*",
/// Port = 80
/// },
/// new WebSite.WebAddress
/// {
/// Address = "*",
/// Port = 80
/// }
/// }
/// </code>
/// </example>
public string AddressesDefinition = "";
//// The iis:WebSite/#Directory attribute must be specified when the element has a Component as an ancestor..
//public string Directory = "";
/// <summary>
/// Reference to a WebApplication that is to be installed as part of this web site.
/// </summary>
public CustomWebApplication WebApplication = null;
/// <summary>
/// Collection of <see cref="T:WebSite.WebAddresses"/> associated with website.
/// <para>
/// The user specified values of <see cref="Addresses"/> will be ignored and replaced with the
/// auto-generated addresses if <see cref="AddressesDefinition"/> is specified either directly or via appropriate <see cref="WebSite"/> constructor.
/// </para>
/// </summary>
public WebAddress[] Addresses
{
get
{
ProcessAddressesDefinition();
return addresses;
}
set
{
addresses = value;
}
}
/// <summary>
/// This class defines WebAppPool WiX element. It is used to specify the application pool for this application in IIS 6 applications.
/// </summary>
public partial class CustomWebApplication : WixEntity
{
/// <summary>
/// References a WebAppPool instance to use as the application pool for this application in IIS 6 applications.
/// </summary>
public WebAppPool WebAppPool; //WebApplication element attribute
/// <summary>
/// Initializes a new instance of the <see cref="WebApplication"/> class.
/// </summary>
/// <param name="name">The name.</param>
/// <param name="attributesDefinition">The attributes definition. This parameter is used to set encapsulated <see cref="T:WixSharp.WixEntity.AttributesDefinition"/>.</param>
public CustomWebApplication(string name, string attributesDefinition)
{
base.Id = $"{name}_WebApplication";
base.Name = name;
base.AttributesDefinition = attributesDefinition;
}
/// <summary>
/// Initializes a new instance of the <see cref="WebAppPool"/> class.
/// </summary>
/// <param name="name">The name.</param>
public CustomWebApplication(string name)
{
base.Id = $"{name}_WebApplication";
base.Name = name;
}
/// <summary>
/// Initializes a new instance of the <see cref="WebAppPool"/> class.
/// </summary>
public CustomWebApplication()
{
}
}
WebAddress[] addresses = new WebAddress[0];
/// <summary>
/// Primary key used to identify this particular entry.
/// </summary>
[Xml]
public new string Id
{
get
{
return base.Id;
}
set
{
base.Id = value;
}
}
/// <summary>
/// The value to set into the environment variable. If this attribute is not set, the environment variable is removed
/// during installation if it exists on the machine.
/// </summary>
[Xml]
public string Description;
/// <summary>
/// Defines the installation <see cref="Condition"/>, which is to be checked during the installation to
/// determine if the registry value should be created on the target system.
/// </summary>
public Condition Condition;
/// <summary>
/// Adds itself as an XML content into the WiX source being generated from the <see cref="WixSharp.Project"/>.
/// See 'Wix#/samples/Extensions' sample for the details on how to implement this interface correctly.
/// </summary>
/// <param name="context">The context.</param>
public void Process(ProcessingContext context)
{
// IIS namespace
XNamespace ns = WixExtension.IIs.ToXNamespace();
XElement component = this.CreateAndInsertParentComponent(context);
component.Add(this.ToXElement(ns + "WebSite"));
XElement webSiteElement = component.FindAll("WebSite")[0];
if (webSiteElement.Parent.Name == "Component" && webSiteElement.Parent.Parent.Name == "Directory")
{
// Add attributes for WebSite element
webSiteElement.AddAttributes($"Directory={webSiteElement.Parent.Parent.Attribute("Id").Value}");
}
if (Addresses != null)
{
int index = 1;
// Generates the XML fragment for WebAddress element
foreach (WebAddress address in Addresses)
{
webSiteElement.AddElement(new XElement(ns + "WebAddress",
new XAttribute("Id", $"WebSite_Address{index}"),
new XAttribute("IP", "*"),
new XAttribute("Port", address.Port)));
index++;
}
}
if (WebApplication != null)
{
// Generates the XML fragment for WebApplication element
XElement webApplicationElement = new XElement(ns + "WebApplication",
new XAttribute("Id", WebApplication.Id),
new XAttribute("Name", this.WebApplication.Name));
webSiteElement.AddElement(webApplicationElement);
if (WebApplication.WebAppPool != null)
{
WebApplication.WebAppPool.Id = $"{WebApplication.WebAppPool.Name}_WebAppPool";
webApplicationElement.SetAttribute($"WebAppPool={WebApplication.WebAppPool.Id}");
// Generates the XML fragment for WebAppPool element
webSiteElement.Parent.AddElement(new XElement(ns + "WebAppPool",
new XAttribute("Id", WebApplication.WebAppPool.Id),
new XAttribute("Name", WebApplication.WebAppPool.Name),
new XAttribute("ManagedPipelineMode", "Integrated"),
new XAttribute("Identity", "applicationPoolIdentity")));
}
}
if (Condition != null)
{
component.AddElement(new XElement("Condition", Condition.ToXValue())
.AddAttributes(Condition.Attributes));
}
}
}
}
You also have other way to solve your problem, by generating the WIX xml file following WebSite definitions and using XML injection as indicated in WixSharp IIS Sample with XMLInjection where you can subscribe to the WixSourceGenerated event.
project.WixSourceGenerated += Compiler_WixSourceGenerated;
Remember that WixSharp generates the Wix XML definitifion file, and you can modify this XML file after the WixSourceGenerated event.