I am using RazorEngine to parse templates from html snippets on a web page. (This is a legacy system that switching to Mvc Razor views isn't possible, so we are switching small sections over to using RazorEngine where it makes sense). There are many of questions on SO and the internet trying to get Mvc's Html and Url helpers to work with Razor engine. To get #Html syntax to work, I've modified some code found here to add Html to the base template:
public HtmlHelper<t> Html
{
get
{
if (helper == null)
{
var writer = this.CurrentWriter; //TemplateBase.CurrentWriter
var vcontext = new ViewContext() { Writer = writer, ViewData = this.ViewData};
helper = new HtmlHelper<t>(vcontext, this);
}
return helper;
}
}
public ViewDataDictionary ViewData
{
get
{
if (viewdata == null)
{
viewdata = new ViewDataDictionary();
viewdata.TemplateInfo = new TemplateInfo() { HtmlFieldPrefix = string.Empty };
if (this.Model != null)
{
viewdata.Model = Model;
}
}
return viewdata;
}
set
{
viewdata = value;
}
}
After a lot of debugging into the Html source code I think I've managed to instantiate everything that the Html helper needs, and it runs successfully for #Html.Label... The problem is that the resulting html is:
<label for="MyNumber">MyNumber</label>
When it obviously should be:
<label for="MyNumber">MyNumber</label>
I am stumped as to how to fix this. I was not able to find how the encoding happens when looking through the RazorEngine source. My initial thought was that the TextWriter must be encoding the value but I have not been able to confirm this. How can I get #Html.BlahFor() to render un-escaped html?
I have found a workaround for the problem I was facing. RazorEngine's base Template will automatically encode a string (or object) if it doesn't cast to an IEncodedString. In my case, I solved this issue by overriding the WriteTo method in my template class:
public override void WriteTo(TextWriter writer, object value)
{
if (writer == null)
throw new ArgumentNullException("writer");
if (value == null) return;
var encodedString = value as IEncodedString;
if (encodedString != null)
{
writer.Write(encodedString);
}
else
{
var htmlString = value as IHtmlString;
if(htmlString != null)
writer.Write(htmlString.ToHtmlString());
else
{
//This was the base template's implementation:
encodedString = TemplateService.EncodedStringFactory.CreateEncodedString(value);
writer.Write(encodedString);
}
}
}
I have yet to see if this change will cause any errors, but now that I found the method it should be relatively easy to change in the future.
EDIT: Checked to see Html helpers return an MvcHtmlString, which implements IHtmlString, so by adding a cast to this interface, we can avoid encoding the html returned by the helpers, while still having a safety in place for any other callers of this method.
Antaris, author of RazorEngine, has supplied another interesting way for handling this issue, and explained by the way why it is not done by default (for reducing as much as possible RazorEngine dependencies).
Here is for your case the relevant part of his answer on the corresponding github issue:
I think the easiest
thing to do here, would be to implement a custom
IEncodedStringFactory which handles MvcHtmlString. Something like:
public class MvcHtmlStringFactory : IEncodedStringFactory
{
public IEncodedString CreateEncodedString(string rawString)
{
return new HtmlEncodedString(rawString);
}
public IEncodedString CreateEncodedString(object obj)
{
if (obj == null)
return new HtmlEncodedString(string.Empty);
var htmlString = obj as HtmlEncodedString;
if (htmlString != null)
return htmlString;
var mvcHtmlString = obj as MvcHtmlString;
if (mvcHtmlString != null)
return new MvcHtmlStringWrapper(mvcHtmlString);
return new HtmlEncodedString(obj.Tostring());
}
}
public MvcHtmlStringWrapper : IEncodedString
{
private readonly MvcHtmlString _value;
public MvcHtmlStringWrapper(MvcHtmlString value)
{
_value = value;
}
public string ToEncodedString()
{
return _value.ToString();
}
public override string ToString()
{
return ToEncodedString();
}
}
This would enable existing MvcHtmlString instances to bypass
RazorEngine's built in encoding mechanism. This would not be part of
the base library because we don't take any dependencies on
System.Web, or System.Web.Mvc.
You would need to wire this up via your configuration:
var config = new TemplateServiceConfiguration()
{
BaseTemplateType = typeof(MvcTemplateBase<>),
EncodedStringFactory = new MvcHtmlStringFactory()
};
Personally, I have switched MvcHtmlString for IHtmlString before using his code. As of MVC 5, MvcHtmlString does implement it too. (Beware, it was not the case in some older MVC version.)
Thanks for the inspiration, I had my data stored in a xml and created a different workaround without overwriting the WriteTo method. I used a class that inherited from the TemplateBase and created 2 new methods.
public IEncodedString HtmlOf(NBrightInfo info, String xpath)
{
var strOut = info.GetXmlProperty(xpath);
strOut = System.Web.HttpUtility.HtmlDecode(strOut);
return new RawString(strOut);
}
public IEncodedString BreakOf(NBrightInfo info, String xpath)
{
var strOut = info.GetXmlProperty(xpath);
strOut = System.Web.HttpUtility.HtmlEncode(strOut);
strOut = strOut.Replace(Environment.NewLine, "<br/>");
strOut = strOut.Replace("\t", " ");
strOut = strOut.Replace("'", "'");
return new RawString(strOut);
}
Related
I'm trying to create my first SSIS custom source component but I can't get it to save the custom properties into the .dtsx file.
According to https://learn.microsoft.com/en-us/sql/integration-services/extending-packages-custom-objects/persisting-custom-objects , all I needed is to implement the IDTSComponentPersist interface, but this doesn't work, the LoadFromXML and SaveToXML are never called. Neither when I save the file nor when I load the package.
However, if your object has properties that use complex data types, or
if you want to perform custom processing on property values as they
are loaded and saved, you can implement the IDTSComponentPersist
interface and its LoadFromXML and SaveToXML methods. In these methods
you load from (or save to) the XML definition of the package an XML
fragment that contains the properties of your object and their current
values. The format of this XML fragment is not defined; it must only
be well-formed XML.
When I save the SSIS package and look inside the XML, I get this, no data type defined and no values :
Did I miss to set something?
To simplify, I created a small test project. The original project try to save a list of struct with 2 string and 1 integer, but both has the same "incorrect" behavior, SaveToXML and LoadFromXML are never called.
Here's my code:
using System;
using System.Collections.Generic;
using Microsoft.SqlServer.Dts.Pipeline.Wrapper;
using Microsoft.SqlServer.Dts.Pipeline;
using Microsoft.SqlServer.Dts.Runtime;
using System.Xml;
using System.ComponentModel;
using System.Globalization;
using System.Drawing.Design;
using System.Windows.Forms.Design;
using System.Windows.Forms;
namespace TestCase
{
public class MyConverter : TypeConverter
{
public override bool GetStandardValuesSupported(ITypeDescriptorContext context)
{
return false;
}
public override object ConvertTo(ITypeDescriptorContext context, CultureInfo culture, object value, Type destinationType)
{
if (destinationType.Name.ToUpper() == "STRING")
return string.Join(",", ((List<string>)value).ToArray());
else
return ((string)value).Split(',');
}
public override object ConvertFrom(ITypeDescriptorContext context, CultureInfo culture, object value)
{
if (value.GetType().Name.ToUpper() == "STRING")
return ((string)value).Split(',');
else
return string.Join(",", ((List<string>)value).ToArray());
}
}
class FancyStringEditor : UITypeEditor
{
public override UITypeEditorEditStyle GetEditStyle(ITypeDescriptorContext context)
{
return UITypeEditorEditStyle.Modal;
}
public override object EditValue(ITypeDescriptorContext context, IServiceProvider provider, object value)
{
var svc = (IWindowsFormsEditorService)provider.GetService(typeof(IWindowsFormsEditorService));
List<string> vals = (List<string>)value;
string valsStr = string.Join("\r\n", vals.ToArray());
if (svc != null)
{
using (var frm = new Form { Text = "Your editor here" })
using (var txt = new TextBox { Text = valsStr, Dock = DockStyle.Fill, Multiline = true })
using (var ok = new Button { Text = "OK", Dock = DockStyle.Bottom })
{
frm.Controls.Add(txt);
frm.Controls.Add(ok);
frm.AcceptButton = ok;
ok.DialogResult = DialogResult.OK;
if (svc.ShowDialog(frm) == DialogResult.OK)
{
vals = new List<string>();
vals.AddRange(txt.Text.Split(new string[] { "\r\n" }, StringSplitOptions.RemoveEmptyEntries));
value = vals;
}
}
}
return value;
}
}
[DtsPipelineComponent(ComponentType = ComponentType.SourceAdapter,
CurrentVersion = 0,
Description = "Test class for saving",
DisplayName = "Test class",
IconResource = "None",
NoEditor = false,
RequiredProductLevel = Microsoft.SqlServer.Dts.Runtime.Wrapper.DTSProductLevel.DTSPL_NONE,
SupportsBackPressure = false,
UITypeName = "None")]
public class TestSave : PipelineComponent, IDTSComponentPersist
{
private string _NbBadWordProperty = "NbBadWord";
private string _ListBadWordsProperty = "ListBadWords";
private List<string> _badWords;
public IDTSCustomProperty100 _nb;
public IDTSCustomProperty100 _list;
public TestSave()
{
_badWords = new List<string>();
_badWords.Add("Word1");
_badWords.Add("Word2");
_badWords.Add("Word3");
}
public void LoadFromXML(System.Xml.XmlElement node, IDTSInfoEvents infoEvents)
{
System.Windows.Forms.MessageBox.Show("Oh god! we're inside LoadFromXML!!");
}
public void SaveToXML(System.Xml.XmlDocument doc, IDTSInfoEvents infoEvents)
{
System.Windows.Forms.MessageBox.Show("Oh god! we're inside SaveToXML!!");
XmlElement elementRoot;
XmlNode propertyNode;
// Create a new node to persist the object and its properties.
elementRoot = doc.CreateElement(String.Empty, "NBElement", String.Empty);
XmlAttribute nbEl = doc.CreateAttribute("Nbelement");
nbEl.Value = _badWords.Count.ToString();
elementRoot.Attributes.Append(nbEl);
// Save the three properties of the object from variables into XML.
foreach (string s in _badWords)
{
propertyNode = doc.CreateNode(XmlNodeType.Element, "BadWord", String.Empty);
propertyNode.InnerText = s;
elementRoot.AppendChild(propertyNode);
}
doc.AppendChild(elementRoot);
}
private IDTSCustomProperty100 GetCustomPropertyByName(string name)
{
foreach (IDTSCustomProperty100 prop in this.ComponentMetaData.CustomPropertyCollection)
if (prop.Name.ToUpper() == name)
return prop;
return null;
}
public override DTSValidationStatus Validate()
{
return DTSValidationStatus.VS_ISVALID;
}
public override void ProvideComponentProperties()
{
try
{
base.ProvideComponentProperties();
// reset the component
this.ComponentMetaData.OutputCollection.RemoveAll();
this.ComponentMetaData.InputCollection.RemoveAll();
// Add custom properties
if (GetCustomPropertyByName(_NbBadWordProperty) == null)
{
_nb = this.ComponentMetaData.CustomPropertyCollection.New();
_nb.Name = _NbBadWordProperty;
_nb.Description = "Number of bad word to filter";
_nb.State = DTSPersistState.PS_DEFAULT;
_nb.Value = _badWords.Count;
_nb.ExpressionType = DTSCustomPropertyExpressionType.CPET_NOTIFY;
}
if (GetCustomPropertyByName(_ListBadWordsProperty) == null)
{
IDTSCustomProperty100 _list = this.ComponentMetaData.CustomPropertyCollection.New();
_list.Name = _ListBadWordsProperty;
_list.Description = "List of bad words";
_list.State = DTSPersistState.PS_DEFAULT;
_list.TypeConverter = typeof(MyConverter).AssemblyQualifiedName;
_list.Value = _badWords;
_list.ExpressionType = DTSCustomPropertyExpressionType.CPET_NOTIFY;
_list.UITypeEditor = typeof(FancyStringEditor).AssemblyQualifiedName;
}
// add input objects
// none
// add output objects
IDTSOutput100 o2 = this.ComponentMetaData.OutputCollection.New();
o2.Name = "Dummy output";
o2.IsSorted = false;
foreach (IDTSCustomProperty100 p in this.ComponentMetaData.CustomPropertyCollection)
{
if (p.Name == _ListBadWordsProperty)
{
MyConverter c = new MyConverter();
List<string> l = (List<string>)p.Value;
foreach (string s in l)
{
IDTSOutputColumn100 col1 = o2.OutputColumnCollection.New();
col1.Name = s.Trim();
col1.Description = "Bad word";
col1.SetDataTypeProperties(Microsoft.SqlServer.Dts.Runtime.Wrapper.DataType.DT_WSTR, 500, 0, 0, 0);
}
}
}
}
catch (Exception ex)
{
System.Windows.Forms.MessageBox.Show("Critical error: " + ex.Message);
}
}
}
}
Update1:
Add the TypeConverter and UITypeEditor. Still the same behavior (not saving the "complex" data type).
When I add the source component to a data flow, I got this, everything look fine:
I can edit the property, no problem
But when I save the SSIS package and look at the xml, the property still not saved and still have a datatype of System.NULL:
Thanks!
Important Note - based on Microsoft definition of IDTSComponentPersist Interface and code samples of SaveToXML found on Internet, I suspect that custom persistence can only be implemented on custom SSIS Tasks, Connection Managers and Enumerators.
Well, please choose for yourself whether do you really need to implement custom object persistence. Your custom properties seems to fit well into standard data types Int32 and String.
Important note from Microsoft -
When you implement custom persistence, you must persist all the properties of the object, including both inherited properties and custom properties that you have added.
So, you really have to do a lot of work to persist all properties of component including LocaleID from your sample - in case someone needs to alter it. I would probably do storing ListBadWords custom property as a string without custom XML persistence.
On your code -- the most possible cause of the System.Null data type problem is that ProvideComponentProperties() method is called on initialization of the component, when it is added on the Data Flow. Data type of the property is determined dynamically at this moment, the variable _badwords is not initialized yet and is a reference type, so it is defined as Null reference. The ProvideComponentProperties() method is used to define custom properties and set its default values, to solve your problem - set
if (GetCustomPropertyByName(_ListBadWordsProperty) == null)
{
IDTSCustomProperty100 _list = this.ComponentMetaData.CustomPropertyCollection.New();
_list.Name = _ListBadWordsProperty;
_list.Description = "List of bad words";
_list.State = DTSPersistState.PS_DEFAULT;
_list.TypeConverter = typeof(MyConverter).AssemblyQualifiedName;
// This is the change
_list.Value = String.Empty;
_list.ExpressionType = DTSCustomPropertyExpressionType.CPET_NOTIFY;
_list.UITypeEditor = typeof(FancyStringEditor).AssemblyQualifiedName;
}
If you set yourself up on implementing custom XML persistence - please study Microsoft code sample and other sources. Saving is done a little bit other way. The main difference is that inside elementRoot of the component properties, each property is created under its own XML Node. Node's InnerText is used to store property value, and optional Node's attributes can store additional information.
Let's assume that I have a view model like this
public class ExampleVM
{
[Display(Name = "Foo")]
public Nullable<decimal> FooInternal { get; set; }
}
My view looks like this (also a form tag which I omitted in this post)
#model ExampleVM
....
<input asp-for="FooInternal" class="form-control" type="number" />
This results in a rendered text box with FooInternal as id-attribute.
In my scenario, I also have a modal dialog with another form with another view model which shares a property with the same name. I know that the asp-for taghelper renders the id-attribute either a manually from a specified id or infers the id from the property name.
In my backend code, I want to be able to name my properties how I seem fit given the view model context. I don't want to rename my properties to make them globally unique.
I try to avoid two things:
To manually specify the id in the view / the input element. I'd much rather use an autogenerated id that I can set via another attribute in the backend.
Given that I use the view model with [FromBody] in a post, I can't exactly rename the property as it would be with [FromRoute(Name="MyFoo")]. I don't want to map a manually entered id back to my property.
Basically, I'm looking for something like this:
public class ExampleVM
{
[Display(Name = "Foo")]
[HtmlId(Name = "MyUniqueFooName")]
public Nullable<decimal> FooInternal { get; set; }
}
where HtmlId would be an attribute that interacts with the tag-helper for rendering and also for rebinding the view model as parameter from a [HttpPost] method.
Maybe another approach is also valid since avoiding multiple input elements (with the same identifier) in multiple forms on the same page seems like a common situation to me.
According to your description, if you want to achieve your requirement, you should write custom modelbinding and custom input tag helper to achieve your requirement.
Since the asp.net core modelbinding will bind the data according to the post back's form data, you should firstly write the custom input tag helper to render the input name property to use HtmlId value.
Then you should write a custom model binding in your project to bind the model according to the HtmlId attribute.
About how to re-write the custom input tag helper, you could refer to below steps:
Notice: Since the input tag helper has multiple type "file, radio,checkbox and else", you should write all the logic based on the source codes.
According to the input taghelper source codes, you could find the tag helper will call the Generator.GenerateTextBox method to generate the input tag html content.
The Generator.GenerateTextBox has five parameters, the third parameter expression is used to generate the input textbox's for attribute.
Generator.GenerateTextBox(
ViewContext,
modelExplorer,
For.Name,
modelExplorer.Model,
format,
htmlAttributes);
If you want to show the HtmlId value as the name for the for attribute, you should create a custom input taghelper.
You should firstly create a custom attribute:
[System.AttributeUsage(AttributeTargets.All, Inherited = false, AllowMultiple = true)]
public class HtmlId : Attribute
{
public string _Id;
public HtmlId(string Id) {
_Id = Id;
}
public string Id
{
get { return _Id; }
}
}
Then you could use var re = ((Microsoft.AspNetCore.Mvc.ModelBinding.Metadata.DefaultModelMetadata)For.ModelExplorer.Metadata).Attributes.PropertyAttributes.Where(x => x.GetType() == typeof(HtmlId)).FirstOrDefault(); to get the htmlid in the input tag helper's GenerateTextBox method.
Details, you could refer to below custom input tag helper codes:
using Microsoft.AspNetCore.Mvc.TagHelpers;
using Microsoft.AspNetCore.Mvc.ViewFeatures;
using Microsoft.AspNetCore.Razor.TagHelpers;
using System;
using System.Collections.Generic;
using System.Linq;
using Microsoft.AspNetCore.Mvc.Rendering;
namespace SecurityRelatedIssue
{
[HtmlTargetElement("input", Attributes = ForAttributeName, TagStructure = TagStructure.WithoutEndTag)]
public class CustomInputTagHelper: InputTagHelper
{
private const string ForAttributeName = "asp-for";
private const string FormatAttributeName = "asp-format";
public override int Order => -10000;
public CustomInputTagHelper(IHtmlGenerator generator)
: base(generator)
{
}
public override void Process(TagHelperContext context, TagHelperOutput output)
{
if (context == null)
{
throw new ArgumentNullException(nameof(context));
}
if (output == null)
{
throw new ArgumentNullException(nameof(output));
}
// Pass through attributes that are also well-known HTML attributes. Must be done prior to any copying
// from a TagBuilder.
if (InputTypeName != null)
{
output.CopyHtmlAttribute("type", context);
}
if (Name != null)
{
output.CopyHtmlAttribute(nameof(Name), context);
}
if (Value != null)
{
output.CopyHtmlAttribute(nameof(Value), context);
}
// Note null or empty For.Name is allowed because TemplateInfo.HtmlFieldPrefix may be sufficient.
// IHtmlGenerator will enforce name requirements.
var metadata = For.Metadata;
var modelExplorer = For.ModelExplorer;
if (metadata == null)
{
throw new InvalidOperationException();
}
string inputType;
string inputTypeHint;
if (string.IsNullOrEmpty(InputTypeName))
{
// Note GetInputType never returns null.
inputType = GetInputType(modelExplorer, out inputTypeHint);
}
else
{
inputType = InputTypeName.ToLowerInvariant();
inputTypeHint = null;
}
// inputType may be more specific than default the generator chooses below.
if (!output.Attributes.ContainsName("type"))
{
output.Attributes.SetAttribute("type", inputType);
}
// Ensure Generator does not throw due to empty "fullName" if user provided a name attribute.
IDictionary<string, object> htmlAttributes = null;
if (string.IsNullOrEmpty(For.Name) &&
string.IsNullOrEmpty(ViewContext.ViewData.TemplateInfo.HtmlFieldPrefix) &&
!string.IsNullOrEmpty(Name))
{
htmlAttributes = new Dictionary<string, object>(StringComparer.OrdinalIgnoreCase)
{
{ "name", Name },
};
}
TagBuilder tagBuilder;
switch (inputType)
{
//case "hidden":
// tagBuilder = GenerateHidden(modelExplorer, htmlAttributes);
// break;
//case "checkbox":
// tagBuilder = GenerateCheckBox(modelExplorer, output, htmlAttributes);
// break;
//case "password":
// tagBuilder = Generator.GeneratePassword(
// ViewContext,
// modelExplorer,
// For.Name,
// value: null,
// htmlAttributes: htmlAttributes);
// break;
//case "radio":
// tagBuilder = GenerateRadio(modelExplorer, htmlAttributes);
// break;
default:
tagBuilder = GenerateTextBox(modelExplorer, inputTypeHint, inputType, htmlAttributes);
break;
}
if (tagBuilder != null)
{
// This TagBuilder contains the one <input/> element of interest.
output.MergeAttributes(tagBuilder);
if (tagBuilder.HasInnerHtml)
{
// Since this is not the "checkbox" special-case, no guarantee that output is a self-closing
// element. A later tag helper targeting this element may change output.TagMode.
output.Content.AppendHtml(tagBuilder.InnerHtml);
}
}
}
private TagBuilder GenerateTextBox(
ModelExplorer modelExplorer,
string inputTypeHint,
string inputType,
IDictionary<string, object> htmlAttributes)
{
var format = Format;
if (string.IsNullOrEmpty(format))
{
if (!modelExplorer.Metadata.HasNonDefaultEditFormat &&
string.Equals("week", inputType, StringComparison.OrdinalIgnoreCase) &&
(modelExplorer.Model is DateTime || modelExplorer.Model is DateTimeOffset))
{
// modelExplorer = modelExplorer.GetExplorerForModel(FormatWeekHelper.GetFormattedWeek(modelExplorer));
}
else
{
//format = GetFormat(modelExplorer, inputTypeHint, inputType);
}
}
if (htmlAttributes == null)
{
htmlAttributes = new Dictionary<string, object>(StringComparer.OrdinalIgnoreCase);
}
htmlAttributes["type"] = inputType;
if (string.Equals(inputType, "file"))
{
htmlAttributes["multiple"] = "multiple";
}
var re = ((Microsoft.AspNetCore.Mvc.ModelBinding.Metadata.DefaultModelMetadata)For.ModelExplorer.Metadata).Attributes.PropertyAttributes.Where(x => x.GetType() == typeof(HtmlId)).FirstOrDefault();
return Generator.GenerateTextBox(
ViewContext,
modelExplorer,
((HtmlId)re).Id,
modelExplorer.Model,
format,
htmlAttributes);
}
}
}
Improt this taghelper in _ViewImports.cshtml
#addTagHelper *,[yournamespace]
Model exmaple:
[Display(Name = "Foo")]
[HtmlId("test")]
public string str { get; set; }
Result:
Then you could write a custom model binding for the model to bind the data according to the htmlid. About how to use custom model binding, you could refer to this article.
I'm working on MVC 5 project and I have an unit test that checks the generated html markup for a custom html helper.
private class Faq
{
[RequiredIfMultiple(new string[] { "SickLeave", "Holidays" }, new object[] { 2, 2 })]
[RequiredIfMultiple(new string[] { "SickLeave1", "SickLeave2", "SickLeave3" }, new object[] { 2, 2, 2 })]
public string Property { get; set; }
}
[Test]
public void HtmlString_With_RequiredIfMultiple_Test()
{
//Arrange
Expression<Func<Faq, string>> expression = (t => t.Property);
//Act
IHtmlString result = htmlHelper.EditorForRequiredIf(expression);
// Assert
Assert.IsTrue(result.ToString().Contains("data-val-requiredifmultiple"));
}
The html helper extension EditorForRequiredIf
public static MvcHtmlString EditorForRequiredIf<TModel, TValue>(this HtmlHelper<TModel> helper
, Expression<Func<TModel, TValue>> expression
, string templateName = null
, string htmlFieldName = null
, object additionalViewData = null)
{
string mvcHtml = EditorExtensions.EditorFor(helper, expression, templateName, htmlFieldName, additionalViewData).ToString();
string element = helper.ViewContext.ViewData.TemplateInfo.GetFullHtmlFieldName(ExpressionHelper.GetExpressionText(expression));
string key = helper.ViewData.Model.ToString() + "." + element;
if (RequiredIfMultipleAttribute.CountPerField != null)
{
RequiredIfMultipleAttribute.CountPerField.Remove(key);
if (RequiredIfMultipleAttribute.CountPerField.Count == 0)
{
RequiredIfMultipleAttribute.CountPerField = null;
}
}
string pattern = #"data\-val\-requiredif[a-z]+";
return Regex.IsMatch(mvcHtml, pattern) ? MergeClientValidationRules(mvcHtml) : MvcHtmlString.Create(mvcHtml);
}
Once in the view, the EditorFor calls GetClientValidationRules method on a custom attribute RequiredIfMultipleAttribute to generate the html markup with proper data-val tags.
GetClientValidationRules method:
public IEnumerable<ModelClientValidationRule> GetClientValidationRules(ModelMetadata metadata, ControllerContext context)
{
int count = 0;
string key = metadata.ContainerType.FullName + "." + metadata.PropertyName;
if (CountPerField == null)
{
CountPerField = new Dictionary<string, int>();
}
if (CountPerField.ContainsKey(key))
{
count = ++CountPerField[key];
}
else
{
CountPerField.Add(key, count);
}
yield return new RequiredIfMultipleValidationRule(ErrorMessageString, Props, Vals, count);
}
So in production, all of this works perfectly. But during tests, I'm having an awful time.
What I get is an empty string on string mvcHtml = EditorExtensions.EditorFor(helper, expression, templateName, htmlFieldName, additionalViewData).ToString(); and on the calls stack, I don't see the method GetClientValidationRules has been called.
On the other hand, if I change EditorExtensions.EditorFor by InputExtensions.TextBoxFor, I see that the MvcHtmlString is correctly generated and GetClientValidationRules was called.
Does anyone have a clue? and I hope I was clear enough.
For the atttribute to work, some piece of code that executes your method has to recognize it and act on it in some way. When you are running your code in production that's what Asp.Net MVC does for you.
Your test is run by NUnit, not by Asp.Net. NUnit doesn't do anything with the attribute because it's not designed to do that. If you think about it, you'll realize that NUnit could not possibly recognize and act on all the various attributes known to all the hosts in which production code may run.
As a result, any use you make of that attribute has to be in your MVC application itself, not in your test.
Intro:
Web application, ASP.NET MVC 3, a controller action that accepts an instance of POCO model class with (potentially) large field.
Model class:
public class View
{
[Required]
[RegularExpression(...)]
public object name { get; set; }
public object details { get; set; }
public object content { get; set; } // the problem field
}
Controller action:
[ActionName(...)]
[Authorize(...)]
[HttpPost]
public ActionResult CreateView(View view)
{
if (!ModelState.IsValid) { return /*some ActionResult here*/;}
... //do other stuff, create object in db etc. return valid result
}
Problem:
An action should be able to accept large JSON objects (at least up to hundred megabytes in a single request and that's no joke). By default I met with several restrictions like httpRuntime maxRequestLength etc. - all solved except MaxJsonLengh - meaning that default ValueProviderFactory for JSON is not capable of handling such objects.
Tried:
Setting
<system.web.extensions>
<scripting>
<webServices>
<jsonSerialization maxJsonLength="2147483647"/>
</webServices>
</scripting>
</system.web.extensions>
does not help.
Creating my own custom ValueProviderFactory as described in #Darin's answer here:
JsonValueProviderFactory throws "request too large"
also failed because I have no possibility to use JSON.Net (due to non-technical reasons). I tried to implement correct deserialization here myself but apparently it's a bit above my knowledge (yet). I was able to deserialize my JSON string to Dictionary<String,Object> here, but that's not what I want - I want to deserialize it to my lovely POCO objects and use them as input parameters for actions.
So, the questions:
Anyone knows better way to overcome the problem without implementing universal custom ValueProviderFactory?
Is there a possibility to specify for what specific controller and action I want to use my custom ValueProviderFactory? If I know the action beforehand than I will be able to deserialize JSON to POCO without much coding in ValueProviderFactory...
I'm also thinking about implementing a custom ActionFilter for that specific problem, but I think it's a bit ugly.
Anyone can suggest a good solution?
The built-in JsonValueProviderFactory ignores the <jsonSerialization maxJsonLength="50000000"/> setting. So you could write a custom factory by using the built-in implementation:
public sealed class MyJsonValueProviderFactory : ValueProviderFactory
{
private static void AddToBackingStore(Dictionary<string, object> backingStore, string prefix, object value)
{
IDictionary<string, object> d = value as IDictionary<string, object>;
if (d != null)
{
foreach (KeyValuePair<string, object> entry in d)
{
AddToBackingStore(backingStore, MakePropertyKey(prefix, entry.Key), entry.Value);
}
return;
}
IList l = value as IList;
if (l != null)
{
for (int i = 0; i < l.Count; i++)
{
AddToBackingStore(backingStore, MakeArrayKey(prefix, i), l[i]);
}
return;
}
// primitive
backingStore[prefix] = value;
}
private static object GetDeserializedObject(ControllerContext controllerContext)
{
if (!controllerContext.HttpContext.Request.ContentType.StartsWith("application/json", StringComparison.OrdinalIgnoreCase))
{
// not JSON request
return null;
}
StreamReader reader = new StreamReader(controllerContext.HttpContext.Request.InputStream);
string bodyText = reader.ReadToEnd();
if (String.IsNullOrEmpty(bodyText))
{
// no JSON data
return null;
}
JavaScriptSerializer serializer = new JavaScriptSerializer();
serializer.MaxJsonLength = 2147483647;
object jsonData = serializer.DeserializeObject(bodyText);
return jsonData;
}
public override IValueProvider GetValueProvider(ControllerContext controllerContext)
{
if (controllerContext == null)
{
throw new ArgumentNullException("controllerContext");
}
object jsonData = GetDeserializedObject(controllerContext);
if (jsonData == null)
{
return null;
}
Dictionary<string, object> backingStore = new Dictionary<string, object>(StringComparer.OrdinalIgnoreCase);
AddToBackingStore(backingStore, String.Empty, jsonData);
return new DictionaryValueProvider<object>(backingStore, CultureInfo.CurrentCulture);
}
private static string MakeArrayKey(string prefix, int index)
{
return prefix + "[" + index.ToString(CultureInfo.InvariantCulture) + "]";
}
private static string MakePropertyKey(string prefix, string propertyName)
{
return (String.IsNullOrEmpty(prefix)) ? propertyName : prefix + "." + propertyName;
}
}
The only modification I did compared to the default factory is adding the following line:
serializer.MaxJsonLength = 2147483647;
Unfortunately this factory is not extensible at all, sealed stuff so I had to recreate it.
and in your Application_Start:
ValueProviderFactories.Factories.Remove(ValueProviderFactories.Factories.OfType<System.Web.Mvc.JsonValueProviderFactory>().FirstOrDefault());
ValueProviderFactories.Factories.Add(new MyJsonValueProviderFactory());
I found that the maxRequestLength did not solve the problem however.
I resolved my issue with the below setting. It is cleaner than having to implement a custom ValueProviderFactory
<appSettings>
<add key="aspnet:MaxJsonDeserializerMembers" value="150000" />
</appSettings>
Credit goes to the following questions:
JsonValueProviderFactory throws "request too large"
Getting "The JSON request was too large to be deserialized"
This setting obviously relates to a highly complex json model and not the actual size.
The solution of Darin Dimitrov works for me but i need reset the position of the stream of the request before read it, adding this line:
controllerContext.HttpContext.Request.InputStream.Position = 0;
So now, the method GetDeserializedObject looks like this:
private static object GetDeserializedObject(ControllerContext controllerContext)
{
if (!controllerContext.HttpContext.Request.ContentType.StartsWith("application/json", StringComparison.OrdinalIgnoreCase))
{
// not JSON request
return null;
}
controllerContext.HttpContext.Request.InputStream.Position = 0;
StreamReader reader = new StreamReader(controllerContext.HttpContext.Request.InputStream);
string bodyText = reader.ReadToEnd();
if (String.IsNullOrEmpty(bodyText))
{
// no JSON data
return null;
}
JavaScriptSerializer serializer = new JavaScriptSerializer();
serializer.MaxJsonLength = 2147483647;
object jsonData = serializer.DeserializeObject(bodyText);
return jsonData;
}
I need to serialize objects to JSON. I would like to do it with a template instead of using data annotations (as most frameworks do). Does anybody know a good way of doing this?
A picture says more than 1000 words. I'm looking for something that looks like this:
For example, if I had a class like this:
public class Test
{
public string Key { get; set; }
public string Name { get; set; }
public string Code { get; set; }
public Test Related { get; set; }
}
And a had template string that could look like this:
{
id: "$Key",
name: "$Name",
related: "$Related.Name"
}
I want to get a JSON object, whose properties are filled in according to Key, Name and Related.Name of the object.
Basically I'm searching for a JSON serialization method that supports templating instead.
I don't know about any library that does this for you, but it's not that hard to build it yourself.
If you have your template, you need to parse it as JSON and then replace all of the placeholders with actual values. To do that, you can use the visitor pattern.
Since JSON.NET (the JSON library I'm using) doesn't seem to have a visitor, you can create one yourself:
abstract class JsonVisitor
{
public virtual JToken Visit(JToken token)
{
var clone = token.DeepClone();
return VisitInternal(clone);
}
protected virtual JToken VisitInternal(JToken token)
{
switch (token.Type)
{
case JTokenType.Object:
return VisitObject((JObject)token);
case JTokenType.Property:
return VisitProperty((JProperty)token);
case JTokenType.Array:
return VisitArray((JArray)token);
case JTokenType.String:
case JTokenType.Integer:
case JTokenType.Float:
case JTokenType.Date:
case JTokenType.Boolean:
case JTokenType.Null:
return VisitValue((JValue)token);
default:
throw new InvalidOperationException();
}
}
protected virtual JToken VisitObject(JObject obj)
{
foreach (var property in obj.Properties())
VisitInternal(property);
return obj;
}
protected virtual JToken VisitProperty(JProperty property)
{
VisitInternal(property.Value);
return property;
}
protected virtual JToken VisitArray(JArray array)
{
foreach (var item in array)
VisitInternal(item);
return array;
}
protected virtual JToken VisitValue(JValue value)
{
return value;
}
}
And then create a specialized visitor that replaces the placeholders with actual values:
class JsonTemplateVisitor : JsonVisitor
{
private readonly object m_data;
private JsonTemplateVisitor(object data)
{
m_data = data;
}
public static JToken Serialize(object data, string templateString)
{
return Serialize(
data, (JToken)JsonConvert.DeserializeObject(templateString));
}
public static JToken Serialize(object data, JToken template)
{
var visitor = new JsonTemplateVisitor(data);
return visitor.Visit(template);
}
protected override JToken VisitValue(JValue value)
{
if (value.Type == JTokenType.String)
{
var s = (string)value.Value;
if (s.StartsWith("$"))
{
string path = s.Substring(1);
var newValue = GetValue(m_data, path);
var newValueToken = new JValue(newValue);
value.Replace(newValueToken);
return newValueToken;
}
}
return value;
}
private static object GetValue(object data, string path)
{
var parts = path.Split('.');
foreach (var part in parts)
{
if (data == null)
break;
data = data.GetType()
.GetProperty(part)
.GetValue(data, null);
}
return data;
}
}
The usage is then simple. For example, with the following template:
{
id : "$Key",
name: "$Name",
additionalInfo:
{
related: [ "$Related.Name" ]
}
}
You can use code like this:
JsonTemplateVisitor.Serialize(data, templateString)
The result then looks like this:
{
"id": "someKey",
"name": "Isaac",
"additionalInfo": {
"related": [
"Arthur"
]
}
}
You might want to add some error-checking, but other than that, the code should work. Also, it uses reflection, so it might not be suitable if performance is important.
10 years have passed since I've posted the question. Since I've been working with Node.JS and discovered Handlebars and how it is pretty easy to get it to parse JSON instead of HTML template. The Handlebars project has been converted to .NET.
You can use a special ITextEncoder to let Handlebars generate JSON:
using HandlebarsDotNet;
using System.Text;
public class JsonTextEncoder : ITextEncoder
{
public void Encode(StringBuilder text, TextWriter target)
{
Encode(text.ToString(), target);
}
public void Encode(string text, TextWriter target)
{
if (text == null || text == "") return;
text = System.Web.HttpUtility.JavaScriptStringEncode(text);
target.Write(text);
}
public void Encode<T>(T text, TextWriter target) where T : IEnumerator<char>
{
var str = text?.ToString();
if (str == null) return;
Encode(str, target);
}
}
Let's see it in action:
using HandlebarsDotNet;
var handlebars = Handlebars.Create();
handlebars.Configuration.TextEncoder = new JsonTextEncoder();
var sourceTemplate = #"{
""id"": ""{{Key}}"",
""name"": ""{{Name}}"",
""related "": ""{{Related.Name}}""
}";
var template = handlebars.Compile(sourceTemplate);
var json = template(new
{
Key = "Alpha",
Name = "Beta",
Related = new
{
Name = "Gamme"
}
});
Console.WriteLine(json);
This will write the following:
{
"id": "Alpha",
"name": "Beta",
"related ": "Gamme"
}
I did a small write-up on the topic on my blog: Handlebars.Net & JSON templates. In this blog I also discuss how to improve debugging these templates.
You can also use a Text Template file for your json template . The template engine will fill in the blanks and return you the result.
If you are using Visual Studio,
Create a .tt file ,
Mark it with TextTemplatingFilePreprocessor in Custom Tool property of the file. This will create a new class for you that takes care of processing the template.
For integrating your data in the resulted string , extend the newly generated class in a separate file , in which you pass the data (the arbitrary class from you image).
Use this to get the json formatted code;
MyData data = ...;
MyTemplatePage page = new MyTemplatePage(data);
String pageContent = page.TransformText();
Now the pageContent have the json formatted string; For more details about how to handle the .tt file , look here : Text Template Control Blocks
I had exactly the same need. I needed an end user (technical users but not developers) to be able to create their own json files that can later be filled via data.
Microsoft Teams is doing something similar with their adaptive card website:
https://adaptivecards.io/designer/
On the bottom left there is a json "template" and on the bottom right a json to load into the template.
Conclusion: Despite extensive research I have not found any .NET library doing this.
Sorry (๑•́ㅿ•̀๑).
Screenshot of adaptive card designer