I'm using Mandrill to send emails, and are using Handlebars to render content in the email.
If I add variables like this everything works fine:
Backend:
message.AddRecipientVariable("test#gmail.com", "MYVALUE", "some value");
Html-Template:
<p>{{MYVALUE}}</p>
But if I try to use {{#each}} - the each section in the sent email is empty. What am I doing wrong here. Do I pass the objStringArray the wrong way or is there somthing else missing out to render the loop?
Backend:
var objList = new List<MyObj> {new MyObj() {Qty = "125"}, new MyObj() { Qty = "16"}};
var jsonSerialiser = new JavaScriptSerializer();
var objStringArray = jsonSerialiser.Serialize(objList.ToArray());
message.AddRecipientVariable("test#gmail.com", "VALUES", objStringArray);
message.merge_language = "handlebars";
_mandrillApi.SendMessage(message, templateName, new List<TemplateContent>());
Html-Template:
<ul>
{{#each VALUES}}
<li>{{Qty}}</li>
{{/each}}
</ul>
Handlebars in mandrill
Mandrill-dotnet
According to the library I'm using (Mandrill-DotNet) the method: AddRecipientVariable() only takes type string as the content variable. And the madrill-API wants a list when doing the {{#each}} -thing. So by changing the library source code a bit, I can pass in a regular List<dynamic>() to AddRecipientVariable-s content-parameter instead, and it works perfect.
By changing source-code:
public struct merge_var
{
#region Fields
/// <summary>
/// The content.
/// </summary>
public string content;
/// <summary>
/// The name.
/// </summary>
public string name;
#endregion
}
public void AddRecipientVariable(string recipient, string name, string content)
{
if (this.merge_vars == null)
{
this.merge_vars = new List<rcpt_merge_var>();
}
rcpt_merge_var entry = this.merge_vars.Where(e => e.rcpt == recipient).FirstOrDefault();
if (entry == null)
{
entry = new rcpt_merge_var { rcpt = recipient };
this.merge_vars.Add(entry);
}
var mv = new merge_var { name = name, content = content };
entry.vars.Add(mv);
}
To this (string content to dynamic content) :
public struct merge_var
{
#region Fields
/// <summary>
/// The content.
/// </summary>
public dynamic content;
/// <summary>
/// The name.
/// </summary>
public string name;
#endregion
}
public void AddRecipientVariable(string recipient, string name, dynamic content)
{
if (this.merge_vars == null)
{
this.merge_vars = new List<rcpt_merge_var>();
}
rcpt_merge_var entry = this.merge_vars.Where(e => e.rcpt == recipient).FirstOrDefault();
if (entry == null)
{
entry = new rcpt_merge_var { rcpt = recipient };
this.merge_vars.Add(entry);
}
var mv = new merge_var { name = name, content = content };
entry.vars.Add(mv);
}
I am working with Djrill in Python, so I a not familiar with the library you are using.
But you should make sure your JSON is not nested in a wrong way as mentioned in {{#each ... }} on Mandrill is resulting in an empty string
Related
I have multi-level json coming from aws secret and I want to bind this json or secret with the configuration of c# so that I can use it in the whole project.
public class AmazonSecretsManagerConfigurationProvider : ConfigurationProvider
{
private readonly string _region;
private readonly string _secretName;
/// <summary>
/// Initializes a new instance of the <see cref="AmazonSecretsManagerConfigurationProvider"/> class.
/// </summary>
/// <param name="region"></param>
/// <param name="secretName"></param>
public AmazonSecretsManagerConfigurationProvider(string region, string secretName)
{
_region = region;
_secretName = secretName;
}
public override void Load()
{
var secret = GetSecret();
Dictionary<string, object> data = JsonConvert.DeserializeObject<Dictionary<string, object>>(secret);
Dictionary<string, string> Data = data.ToDictionary(k => k.Key, k => k.Value.ToString());
}
private string GetSecret()
{
var request = new GetSecretValueRequest
{
SecretId = "awsseceret",
VersionStage = "AWSCURRENT" // VersionStage defaults to AWSCURRENT if unspecified.
};
string accesskey = "################";
string secretkey = "#######################";
using (var client =
new AmazonSecretsManagerClient(accesskey, secretkey, RegionEndpoint.GetBySystemName(this._region)))
{
var response = client.GetSecretValueAsync(request).Result;
string secretString;
if (response.SecretString != null)
{
secretString = response.SecretString;
}
else
{
var memoryStream = response.SecretBinary;
var reader = new StreamReader(memoryStream);
secretString =
System.Text.Encoding.UTF8
.GetString(Convert.FromBase64String(reader.ReadToEnd()));
}
return secretString;
}
}
}
}
I get aws secretString in the secret variable but how can I bind this to the configuration?
I am trying to make a winform application. The app has 2 textboxes (firstName, lastName), a numericUpDown, and a checkbox. The app is able to read from a text file, with comma separated rows (Daniel,Brown,26,true). The app put this info in a listbox. Then you can add a new user. When you are finished adding users you press save and the new info from lisbox will be saved in that text file. I've created the read file script and add user succesfully. However I can't create the save user button so that it'll save: Daniel,Brown,26,true. I was able to save as: Daniel,Brown,26,happy.
Here is the Person Class:
public class Person
{
public string FirstName { get; set; }
public string LastName { get; set; }
public string Age { get; set; }
public bool IsHappy { get; set; }
public override string ToString()
{
var statusText = IsHappy ? "happy" : "not happy";
return $"{FirstName} {LastName} is {Age} and is {statusText}";
}
}
Here is the form.cs with it's script:
public partial class ChallengeForm : Form
{
private BindingList<Person> _persons = new BindingList<Person>();
private PersonsService _personsService;
public ChallengeForm()
{
_personsService = new PersonsService();
InitializeComponent();
WireUpDropDown();
}
private void WireUpDropDown()
{
_persons = new BindingList<Person>(_personsService.GetPersons(#"C:\Users\user\Desktop\Document.TXT"));
usersListBox.DataSource = _persons;
}
private void addUserButton_Click(object sender, EventArgs e)
{
var person = new Person { FirstName = firstNameText.Text, LastName = lastNameText.Text, Age = agePicker.Text, IsHappy = isHappyCheckbox.Checked };
_persons.Add(person);
}
private void saveListButton_Click(object sender, EventArgs e)
{
}
}
My question is how can I convert the status back to bool. And write the listbox to the text file as csv. I would be very thankfull if you could use SoC.
Here is what I've tried:
const string sPath = (#"C:\Users\user\Desktop\Document.TXT");
System.IO.StreamWriter SaveFile = new System.IO.StreamWriter(sPath);
SaveFile.Write(myperson);
foreach (var item in usersListBox.Items)
{
List<string> unwantedWords = new List<string> { "is", "and" };
var linesSplitted = item.ToString().Split(' ').ToList();
var wordsWithoutUnwantedWords = linesSplitted.Where(i => !unwantedWords.Contains(i)).ToList();
for (int i = 0; i<wordsWithoutUnwantedWords.Count; i++)
{
var isLastWord = i == wordsWithoutUnwantedWords.Count - 1;
SaveFile.Write(wordsWithoutUnwantedWords[i]);
if (!isLastWord)
{
SaveFile.Write(",");
}
Look into XML Serialization. You can just pass in your filepath and the object and the rest will be done for you by the Serialisation classes. Code below:
This is taken from DeadlyDog's awesome answer: https://stackoverflow.com/a/22417240/1623971
/// <summary>
/// Writes the given object instance to an XML file.
/// <para>Only Public properties and variables will be written to the file. These can be any type though, even other classes.</para>
/// <para>If there are public properties/variables that you do not want written to the file, decorate them with the [XmlIgnore] attribute.</para>
/// <para>Object type must have a parameterless constructor.</para>
/// </summary>
/// <typeparam name="T">The type of object being written to the file.</typeparam>
/// <param name="filePath">The file path to write the object instance to.</param>
/// <param name="objectToWrite">The object instance to write to the file.</param>
/// <param name="append">If false the file will be overwritten if it already exists. If true the contents will be appended to the file.</param>
public static void WriteToXmlFile<T>(string filePath, T objectToWrite, bool append = false) where T : new()
{
TextWriter writer = null;
try
{
var serializer = new XmlSerializer(typeof(T));
writer = new StreamWriter(filePath, append);
serializer.Serialize(writer, objectToWrite);
}
finally
{
if (writer != null)
writer.Close();
}
}
/// <summary>
/// Reads an object instance from an XML file.
/// <para>Object type must have a parameterless constructor.</para>
/// </summary>
/// <typeparam name="T">The type of object to read from the file.</typeparam>
/// <param name="filePath">The file path to read the object instance from.</param>
/// <returns>Returns a new instance of the object read from the XML file.</returns>
public static T ReadFromXmlFile<T>(string filePath) where T : new()
{
TextReader reader = null;
try
{
var serializer = new XmlSerializer(typeof(T));
reader = new StreamReader(filePath);
return (T)serializer.Deserialize(reader);
}
finally
{
if (reader != null)
reader.Close();
}
}
Sample use case:
WriteToXmlFile<Person>("C:\someClass.txt", objectToSerialize);
// Read the file contents back into a variable.
Person deserializedObject = ReadFromXmlFile<Person>("C:\someClass.txt");
I can't deserialize JSON file wich contain multiple objects of type Account. Here is how I build my data.json file. I have a List of Accounts List<Account> which is serialize as usual. I separate each object in json file by adding a new line.
List<Account> listOfAccounts = new List<Account>();
if (listOfAccounts != null)
{
foreach (var item in listOfAccounts)
{
JavaScriptSerializer ser = new JavaScriptSerializer();
string outputJSON = ser.Serialize(item);
// I added Environmetn.NewLine to separate each account in json file.
File.AppendAllText("data.json", outputJSON + Environment.NewLine);
}
}
Now, when I'm trying to retrieve all Accounts from the JSON file to List<Account> I get an error: System.ArgumentException: 'Invalid JSON primitive:
Please help to find the solutin, so I can get all objects from the JSON file.
private void JsonFileLoad()
{
if(File.Exists("data.json"))
{
String JSONtxt = File.ReadAllText("data.json");
JavaScriptSerializer ser = new JavaScriptSerializer();
// Not sure how to deserialize all lines into List<account>
List<Account> desirializedAccounts = ser.Deserialize<List<Account>>(JSONtxt);
}
}
I really appreciate any help you can provide.
First of all, you should be serializing in standard json format. What you are doing is serializing the object and writing to file. It should have been, serialize the list altogether to json format.
But assuming you might be having specific reason, I have given many possible solutions in different scenarios.
Note: I have used Newtonsoft.Json as this is fast and I find it very easy.
http://www.newtonsoft.com/json
https://www.nuget.org/packages/newtonsoft.json/
Solutions:
//Class for testing object
public class Account
{
public int Id { get; set; }
public string Name { get; set; }
}
Solution 1: Recommended
/// <summary>
/// Serializing the list in single go
/// </summary>
public void Serialize()
{
List<Account> listOfAccounts = new List<Account>();
listOfAccounts.Add(new Account { Id = 1, Name = "First" });
listOfAccounts.Add(new Account { Id = 2, Name = "Second" });
listOfAccounts.Add(new Account { Id = 3, Name = "Third" });
string outputJSON = Newtonsoft.Json.JsonConvert.SerializeObject(listOfAccounts, Newtonsoft.Json.Formatting.Indented);
File.WriteAllText(#"c:\temp\data.json", outputJSON + Environment.NewLine);
}
/// <summary>
/// Serializing the list, one by one object
/// Comma is appended to every object in json format
/// Finally, enclosed it with [ and ] to make it array of objects
/// </summary>
public void Serialize2()
{
List<Account> listOfAccounts = new List<Account>();
listOfAccounts.Add(new Account { Id = 1, Name = "First" });
listOfAccounts.Add(new Account { Id = 2, Name = "Second" });
listOfAccounts.Add(new Account { Id = 3, Name = "Third" });
string outputJSON = "";
foreach(var item in listOfAccounts)
{
outputJSON += Newtonsoft.Json.JsonConvert.SerializeObject(item, Newtonsoft.Json.Formatting.Indented)+",";
}
File.WriteAllText(#"c:\temp\data.json", "["+outputJSON + "]");
}
/// <summary>
/// Read serialized data into list of objects
/// </summary>
public void DeSerialize()
{
if (File.Exists(#"c:\temp\data.json"))
{
String JSONtxt = File.ReadAllText(#"c:\temp\data.json");
var accounts = Newtonsoft.Json.JsonConvert.DeserializeObject<IEnumerable<Account>>(JSONtxt);
}
}
Solution 2: as per your requirement
/// <summary>
/// Non standard json serialization (object one by one) Highly discouraged unless you have specific reason
/// Assuming the output will not have internal objects
/// </summary>
public void SerializeNonStandard()
{
List<Account> listOfAccounts = new List<Account>();
listOfAccounts.Add(new Account { Id = 1, Name = "First" });
listOfAccounts.Add(new Account { Id = 2, Name = "Second" });
listOfAccounts.Add(new Account { Id = 3, Name = "Third" });
foreach (var item in listOfAccounts)
{
string outputJSON = Newtonsoft.Json.JsonConvert.SerializeObject(item, Newtonsoft.Json.Formatting.Indented);
File.AppendAllText(#"c:\temp\data-ns.json", outputJSON + Environment.NewLine);
}
}
/// <summary>
/// Deserializes the list in one by one fashion and appends to list
/// </summary>
public void DeSerializeNonStandard()
{
if (File.Exists(#"c:\temp\data-ns.json"))
{
List<Account> listOfAccounts = new List<Account>();
String JSONtxt = File.ReadAllText(#"c:\temp\data-ns.json");
//Capture JSON string for each object, including curly brackets
Regex regex = new Regex(#".*(?<=\{)[^}]*(?=\}).*", RegexOptions.IgnoreCase);
MatchCollection matches = regex.Matches(JSONtxt);
foreach(Match match in matches)
{
string objStr = match.ToString();
Account account = Newtonsoft.Json.JsonConvert.DeserializeObject<Account>(objStr);
listOfAccounts.Add(account);
}
}
}
/// <summary>
/// Deserializes the non standard json to list of accounts
/// Splits the object strings, merges with comma and encloses with [] to make it array of objects format and deserializes
/// </summary>
public void DeSerializeNonStandardList()
{
if (File.Exists(#"c:\temp\data-ns.json"))
{
String JSONtxt = File.ReadAllText(#"c:\temp\data-ns.json");
//Capture JSON string for each object, including curly brackets
Regex regex = new Regex(#".*(?<=\{)[^}]*(?=\}).*", RegexOptions.IgnoreCase);
MatchCollection matches = regex.Matches(JSONtxt);
string joinedJSON = string.Join(",", matches.Cast<Match>().Select(m => m.Value));
joinedJSON = string.Format("[{0}]", joinedJSON);
var listOfAccounts = Newtonsoft.Json.JsonConvert.DeserializeObject<IEnumerable<Account>>(joinedJSON);
}
}
You are serializing Account and not List of Account that's why deserialize it failed. Do you need to separate those account in new line? if so you could try add char '[' in the 1st and last char ']' in data.json.
I'm working on Developing a Web-API project, and i'm very impressed with the auto generated documentation by Microsoft's HelpPages.
i enabled custom documentation using the official site creating Help Pages
the documentation is generated successfully BUT none of the references to Classes from the <See cref=""> Tag seems to be added to the description, the HelpPages Simply ignores them (that's for a reason).
i really wanted to have this feature in my project, i searched a lot (got close sometimes) but none gave a convincing answer.
That's why i decided to post my solution to this tweak and hopefully benefit other programmers and spare them some time and effort.
(my answer is in the replies below)
my solution is the following:
you've got your custom documentation (from the generated xml file) working
enable HTML and XML tags within the documentation, they normally get filtered out, thanks this Post you can preserve them.
simply go to: ProjectName > Areas > HelpPage > XmlDocumentationProvider.cs
on line 123 in method: GetTagValue(XPathNavigator parentNode, string tagName)
change the code return node.Value.Trim(); to return node.InnerXml;
create the following partial view:
ProjectName\Areas\HelpPage\Views\Help**_XML_SeeTagsRenderer.cshtml**
this is my code:
#using System.Web.Http;
#using MyProject.Areas.HelpPage.Controllers;
#using MyProject.Areas.HelpPage;
#using MyProject.Areas.HelpPage.ModelDescriptions
#using System.Text.RegularExpressions
#model string
#{
int #index = 0;
string #xml = Model;
if (#xml == null)
#xml = "";
Regex #seeRegex = new Regex("<( *)see( +)cref=\"([^\"]):([^\"]+)\"( *)/>");//Regex("<see cref=\"T:([^\"]+)\" />");
Match #xmlSee = #seeRegex.Match(#xml);
string #typeAsText = "";
Type #tp;
ModelDescriptionGenerator modelDescriptionGenerator = (new HelpController()).Configuration.GetModelDescriptionGenerator();
}
#if (xml !="" && xmlSee != null && xmlSee.Length > 0)
{
while (xmlSee != null && xmlSee.Length > 0)
{
#MvcHtmlString.Create(#xml.Substring(#index, #xmlSee.Index - #index))
int startingIndex = xmlSee.Value.IndexOf(':')+1;
int endIndex = xmlSee.Value.IndexOf('"', startingIndex);
typeAsText = xmlSee.Value.Substring(startingIndex, endIndex - startingIndex); //.Replace("<see cref=\"T:", "").Replace("\" />", "");
System.Reflection.Assembly ThisAssembly = typeof(ThisProject.Controllers.HomeController).Assembly;
tp = ThisAssembly.GetType(#typeAsText);
if (tp == null)//try another referenced project
{
System.Reflection.Assembly externalAssembly = typeof(MyExternalReferncedProject.AnyClassInIt).Assembly;
tp = externalAssembly.GetType(#typeAsText);
}
if (tp == null)//also another referenced project- as needed
{
System.Reflection.Assembly anotherExtAssembly = typeof(MyExternalReferncedProject2.AnyClassInIt).Assembly;
tp = anotherExtAssembly .GetType(#typeAsText);
}
if(tp == null)//case of nested class
{
System.Reflection.Assembly thisAssembly = typeof(ThisProject.Controllers.HomeController).Assembly;
//the below code is done to support detecting nested classes.
var processedTypeString = typeAsText;
var lastIndexofPoint = typeAsText.LastIndexOf('.');
while (lastIndexofPoint > 0 && tp == null)
{
processedTypeString = processedTypeString.Insert(lastIndexofPoint, "+").Remove(lastIndexofPoint + 1, 1);
tp = SPLocatorBLLAssembly.GetType(processedTypeString);//nested class are recognized as: namespace.outerClass+nestedClass
lastIndexofPoint = processedTypeString.LastIndexOf('.');
}
}
if (#tp != null)
{
ModelDescription md = modelDescriptionGenerator.GetOrCreateModelDescription(tp);
#Html.DisplayFor(m => md.ModelType, "ModelDescriptionLink", new { modelDescription = md })
}
else
{
#MvcHtmlString.Create(#typeAsText)
}
index = xmlSee.Index + xmlSee.Length;
xmlSee = xmlSee.NextMatch();
}
#MvcHtmlString.Create(#xml.Substring(#index, #xml.Length - #index))
}
else
{
#MvcHtmlString.Create(#xml);
}
Finally Go to: ProjectName\Areas\HelpPage\Views\Help\DisplayTemplates**Parameters.cshtml**
at line '20' we have the code corresponding to the Description in the documentation.
REPLACE this:
<td class="parameter-documentation">
<p>
#parameter.Documentation
</p>
</td>
With THIS:
<td class="parameter-documentation">
<p>
#Html.Partial("_XML_SeeTagsRenderer", (#parameter.Documentation == null? "" : #parameter.Documentation.ToString()))
</p>
</td>
& Voila you must have it working now.
Notes:
i tried putting HTML list inside the docs and it rendered it fine
i tried multiple class references (multiple <see cref="MyClass"> and i worked fine
you can't refer to a class that is declared within a class
when you refer to a class that is outside the current project please add the assembly .getType of a class within that project (check my code above)
any un-found class found inside a <see cref> will have it's full name printed in the description (for example if you reference a property or namespace, the code won't identify it as a type/class but it will be printed)
I have implemented class which process xml documentation block and changes documentation tags to html tags.
/// <summary>
/// Reprensets xml help block converter interface.
/// </summary>
public class HelpBlockRenderer : IHelpBlockRenderer
{
/// <summary>
/// Stores regex to parse <c>See</c> tag.
/// </summary>
private static readonly Regex regexSeeTag = new Regex("<( *)see( +)cref=\"(?<prefix>[^\"]):(?<member>[^\"]+)\"( *)/>",
RegexOptions.IgnoreCase);
/// <summary>
/// Stores the pair tag coversion dictionary.
/// </summary>
private static readonly Dictionary<string, string> pairedTagConvertsion = new Dictionary<string, string>()
{
{ "para", "p" },
{ "c", "b" }
};
/// <summary>
/// Stores configuration.
/// </summary>
private HttpConfiguration config;
/// <summary>
/// Initializes a new instance of the <see cref="HelpBlockRenderer"/> class.
/// </summary>
/// <param name="config">The configuration.</param>
public HelpBlockRenderer(HttpConfiguration config)
{
this.config = config;
}
/// <summary>
/// Initializes a new instance of the <see cref="HelpBlockRenderer"/> class.
/// </summary>
public HelpBlockRenderer()
: this(GlobalConfiguration.Configuration)
{
}
/// <summary>
/// Renders specified xml help block to valid html content.
/// </summary>
/// <param name="helpBlock">The help block.</param>
/// <param name="urlHelper">The url helper for link building.</param>
/// <returns>The html content.</returns>
public HtmlString RenderHelpBlock(string helpBlock, UrlHelper urlHelper)
{
if (string.IsNullOrEmpty(helpBlock))
{
return new HtmlString(string.Empty);
}
string result = helpBlock;
result = this.RenderSeeTag(result, urlHelper);
result = this.RenderPairedTags(result);
return new HtmlString(result);
}
/// <summary>
/// Process <c>See</c> tag.
/// </summary>
/// <param name="helpBlock">Hte original help block string.</param>
/// <param name="urlHelper">The url helper for link building.</param>
/// <returns>The html content.</returns>
private string RenderSeeTag(string helpBlock, UrlHelper urlHelper)
{
string result = helpBlock;
Match match = null;
while ((match = HelpBlockRenderer.regexSeeTag.Match(result)).Success)
{
var originalValues = match.Value;
var prefix = match.Groups["prefix"].Value;
var anchorText = string.Empty;
var link = string.Empty;
switch (prefix)
{
case "T":
{
// if See tag has reference to type, then get value from member regex group.
var modelType = match.Groups["member"].Value;
anchorText = modelType.Substring(modelType.LastIndexOf(".") + 1);
link = urlHelper.Action("ResourceModel", "Help",
new
{
modelName = anchorText,
area = "ApiHelpPage"
});
break;
}
case "M":
{
// Check that specified type member is API member.
var apiDescriptor = this.GetApiDescriptor(match.Groups["member"].Value);
if (apiDescriptor != null)
{
anchorText = apiDescriptor.ActionDescriptor.ActionName;
link = urlHelper.Action("Api", "Help",
new
{
apiId = ApiDescriptionExtensions.GetFriendlyId(apiDescriptor),
area = "ApiHelpPage"
});
}
else
{
// Web API Help can generate help only for whole API model,
// So, in case if See tag contains link to model member, replace link with link to model class.
var modelType = match.Groups["member"].Value.Substring(0, match.Groups["member"].Value.LastIndexOf("."));
anchorText = modelType.Substring(modelType.LastIndexOf(".") + 1);
link = urlHelper.Action("ResourceModel", "Help",
new
{
modelName = anchorText,
area = "ApiHelpPage"
});
}
break;
}
default:
{
anchorText = match.Groups["member"].Value;
// By default link will be rendered with empty anrchor.
link = "#";
break;
}
}
// Build the anchor.
var anchor = string.Format("{1}", link, anchorText);
result = result.Replace(originalValues, anchor);
}
return result;
}
/// <summary>
/// Converts original help paired tags to html tags.
/// </summary>
/// <param name="helpBlock">The help block.</param>
/// <returns>The html content.</returns>
private string RenderPairedTags(string helpBlock)
{
var result = helpBlock;
foreach (var key in HelpBlockRenderer.pairedTagConvertsion.Keys)
{
Regex beginTagRegex = new Regex(string.Format("<{0}>", key), RegexOptions.IgnoreCase);
Regex endTagRegex = new Regex(string.Format("</{0}>", key), RegexOptions.IgnoreCase);
result = beginTagRegex.Replace(result, string.Format("<{0}>", HelpBlockRenderer.pairedTagConvertsion[key]));
result = endTagRegex.Replace(result, string.Format("</{0}>", HelpBlockRenderer.pairedTagConvertsion[key]));
}
return result;
}
/// <summary>
/// Gets the api descriptor by specified member name.
/// </summary>
/// <param name="member">The member fullname.</param>
/// <returns>The api descriptor.</returns>
private ApiDescription GetApiDescriptor(string member)
{
Regex controllerActionRegex = new Regex("[a-zA-Z0-9\\.]+\\.(?<controller>[a-zA-Z0-9]+)Controller\\.(?<action>[a-zA-Z0-9]+)\\(.*\\)");
var match = controllerActionRegex.Match(member);
if (match.Success)
{
var controller = match.Groups["controller"].Value;
var action = match.Groups["action"].Value;
var descriptions = this.config.Services.GetApiExplorer().ApiDescriptions;
return descriptions.FirstOrDefault(x => x.ActionDescriptor.ActionName.Equals(action) &&
x.ActionDescriptor.ControllerDescriptor.ControllerName == controller);
}
return null;
}
}
To use it, you will need to change XmlDocumentationProvider class:
private static string GetTagValue(XPathNavigator parentNode, string tagName)
{
if (parentNode != null)
{
XPathNavigator node = parentNode.SelectSingleNode(tagName);
if (node != null)
{
return node.InnerXml;
}
}
return null;
}
And then I wrote extension class to use this class directly from view:
/// <summary>
/// Represents html help content extension class.
/// Contains methods to convert Xml help blocks to html string.
/// </summary>
public static class HtmlHelpContentExtensions
{
/// <summary>
/// Converts help block in xml format to html string with proper tags, links and etc.
/// </summary>
/// <param name="helpBlock">The help block content.</param>
/// <param name="urlHelper">The url helper for link building.</param>
/// <returns>The resulting html string.</returns>
public static HtmlString ToHelpContent(this string helpBlock, UrlHelper urlHelper)
{
// Initialize your rendrer here or take it from IoC
return renderer.RenderHelpBlock(helpBlock, urlHelper);
}
}
And finally, for example in Parameters.cshtml:
<td class="parameter-documentation">
<p>#parameter.Documentation.ToHelpContent(Url)</p>
#if(!string.IsNullOrEmpty(parameter.Remarks))
{
<p>#parameter.Remarks.ToHelpContent(Url)</p>
}
</td>
I followed this article and I created my CustomSettingsProvider in order to get rid of the "_url_somehash" part of the path where the user.config file is stored. Now my settings are stored in <LocalApplicationData>\CompanyName\ProductName\Version\user.config as I wanted.
My user.config file (written by my application before creating my CustomSettingsProvider) contains one Int32[] property that was stored and loaded correctly by the default SettingsProvider. When I use my CustomSettingsProvider I get the following exception:
Exception InvalidCastException
Source = mscorlib
Message = Invalid cast from 'System.String' to 'System.Int32[]'.
TargetSite = System.Object DefaultToType(System.IConvertible, System.Type, System.IFormatProvider)
Stack =
System.Convert.DefaultToType(IConvertible value, Type targetType, IFormatProvider provider)
System.String.System.IConvertible.ToType(Type type, IFormatProvider provider)
System.Convert.ChangeType(Object value, Type conversionType, IFormatProvider provider)
System.Convert.ChangeType(Object value, Type conversionType)
MyApp.Interface.CustomSettingsProvider.GetPropertyValues(SettingsContext context, SettingsPropertyCollection collection) in d:\Users\angelo\Documents\Visual Studio 2013\Projects\MyApp\MyApp\Interface\CustomSettingsProvider.cs:line 112
System.Configuration.SettingsBase.GetPropertiesFromProvider(SettingsProvider provider)
System.Configuration.SettingsBase.GetPropertyValueByName(String propertyName)
System.Configuration.SettingsBase.get_Item(String propertyName)
System.Configuration.ApplicationSettingsBase.GetPropertyValue(String propertyName)
System.Configuration.ApplicationSettingsBase.get_Item(String propertyName)
MyApp.Properties.Settings.get_UpgradeRequired() in d:\Users\angelo\Documents\Visual Studio 2013\Projects\MyApp\MyApp\Properties\Settings.Designer.cs:line 31
MyApp.Interface.Program.Run() in d:\Users\angelo\Documents\Visual Studio 2013\Projects\MyApp\MyApp\Interface\Program.cs:line 51
MyApp.Interface.Program.Main() in d:\Users\angelo\Documents\Visual Studio 2013\Projects\MyApp\MyApp\Interface\Program.cs:line 34
How can I fix this problem? In a more general way, how can I store collections and classes in the same way I can do it with the default SettingsProvider?
This is the full code of my CustomSettingsProvider class:
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Configuration;
using System.Reflection;
using System.Xml.Linq;
using System.IO;
// ==>>>> https://stackoverflow.com/questions/2265271/custom-path-of-the-user-config
// https://stackoverflow.com/questions/1947185/c-sharp-get-special-folder
namespace MyApp.Interface
{
class CustomSettingsProvider : SettingsProvider
{
#region Helper struct
/// <summary>
/// Helper struct.
/// </summary>
internal struct SettingStruct
{
internal string name;
internal string serializeAs;
internal string value;
}
#endregion
#region Constants
const string NAME = "name";
const string SERIALIZE_AS = "serializeAs";
const string CONFIG = "configuration";
const string USER_SETTINGS = "userSettings";
const string SETTING = "setting";
#endregion
#region Fields
bool _loaded;
#endregion
#region Properties
/// <summary>
/// Override.
/// </summary>
public override string ApplicationName { get { return System.Reflection.Assembly.GetExecutingAssembly().ManifestModule.Name; } set { /*do nothing*/ } }
/// <summary>
/// The setting key this is returning must set before the settings are used.
/// e.g. <c>Properties.Settings.Default.SettingsKey = #"C:\temp\user.config";</c>
/// </summary>
private string UserConfigPath
{
get
{
System.Diagnostics.FileVersionInfo versionInfo;
string strUserConfigPath, strUserConfigFolder;
strUserConfigPath = Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData, Environment.SpecialFolderOption.Create);
versionInfo = System.Diagnostics.FileVersionInfo.GetVersionInfo(Assembly.GetEntryAssembly().Location);
strUserConfigPath = Path.Combine(strUserConfigPath, versionInfo.CompanyName, versionInfo.ProductName, versionInfo.ProductVersion, "user.config");
strUserConfigFolder = Path.GetDirectoryName(strUserConfigPath);
if(!Directory.Exists(strUserConfigFolder))
Directory.CreateDirectory(strUserConfigFolder);
return strUserConfigPath;
}
}
/// <summary>
/// In memory storage of the settings values
/// </summary>
private Dictionary<string, SettingStruct> SettingsDictionary { get; set; }
#endregion
#region Constructor
/// <summary>
/// Loads the file into memory.
/// </summary>
public CustomSettingsProvider()
{
SettingsDictionary = new Dictionary<string, SettingStruct>();
}
/// <summary>
/// Override.
/// </summary>
public override void Initialize(string name, System.Collections.Specialized.NameValueCollection config)
{
base.Initialize(ApplicationName, config);
}
#endregion
/// <summary>
/// Must override this, this is the bit that matches up the designer properties to the dictionary values
/// </summary>
/// <param name="context"></param>
/// <param name="collection"></param>
/// <returns></returns>
public override SettingsPropertyValueCollection GetPropertyValues(SettingsContext context, SettingsPropertyCollection collection)
{
//load the file
if(!_loaded)
{
_loaded = true;
LoadValuesFromFile();
}
//collection that will be returned.
SettingsPropertyValueCollection values = new SettingsPropertyValueCollection();
//iterate thought the properties we get from the designer, checking to see if the setting is in the dictionary
foreach(SettingsProperty setting in collection)
{
SettingsPropertyValue value = new SettingsPropertyValue(setting);
value.IsDirty = false;
//need the type of the value for the strong typing
var t = Type.GetType(setting.PropertyType.FullName);
if(SettingsDictionary.ContainsKey(setting.Name))
{
value.SerializedValue = SettingsDictionary[setting.Name].value;
value.PropertyValue = Convert.ChangeType(SettingsDictionary[setting.Name].value, t);
}
else //use defaults in the case where there are no settings yet
{
value.SerializedValue = setting.DefaultValue;
value.PropertyValue = Convert.ChangeType(setting.DefaultValue, t);
}
values.Add(value);
}
return values;
}
/// <summary>
/// Must override this, this is the bit that does the saving to file. Called when Settings.Save() is called
/// </summary>
/// <param name="context"></param>
/// <param name="collection"></param>
public override void SetPropertyValues(SettingsContext context, SettingsPropertyValueCollection collection)
{
//grab the values from the collection parameter and update the values in our dictionary.
foreach(SettingsPropertyValue value in collection)
{
var setting = new SettingStruct()
{
value = (value.PropertyValue == null ? String.Empty : value.PropertyValue.ToString()),
name = value.Name,
serializeAs = value.Property.SerializeAs.ToString()
};
if(!SettingsDictionary.ContainsKey(value.Name))
SettingsDictionary.Add(value.Name, setting);
else
SettingsDictionary[value.Name] = setting;
}
//now that our local dictionary is up-to-date, save it to disk.
SaveValuesToFile();
}
/// <summary>
/// Loads the values of the file into memory.
/// </summary>
private void LoadValuesFromFile()
{
string strUserConfigPath;
strUserConfigPath = UserConfigPath;
//if the config file is not where it's supposed to be create a new one.
if(!File.Exists(strUserConfigPath))
CreateEmptyConfig(strUserConfigPath);
//System.Security.Policy.StrongName strongName = new System.Security.Policy.StrongName(
//ClickOnce
//load the xml
var configXml = XDocument.Load(UserConfigPath);
//get all of the <setting name="..." serializeAs="..."> elements.
var settingElements = configXml.Element(CONFIG).Element(USER_SETTINGS).Element(typeof(Properties.Settings).FullName).Elements(SETTING);
//iterate through, adding them to the dictionary, (checking for nulls, xml no likey nulls)
//using "String" as default serializeAs...just in case, no real good reason.
foreach(var element in settingElements)
{
var newSetting = new SettingStruct()
{
name = element.Attribute(NAME) == null ? String.Empty : element.Attribute(NAME).Value,
serializeAs = element.Attribute(SERIALIZE_AS) == null ? "String" : element.Attribute(SERIALIZE_AS).Value,
value = element.Value ?? String.Empty
};
SettingsDictionary.Add(element.Attribute(NAME).Value, newSetting);
}
}
/// <summary>
/// Creates an empty user.config file...looks like the one MS creates.
/// This could be overkill a simple key/value pairing would probably do.
/// </summary>
private void CreateEmptyConfig(string strUserConfigPath)
{
Configuration config1;
config1 = ConfigurationManager.OpenExeConfiguration(ConfigurationUserLevel.PerUserRoamingAndLocal);
if(File.Exists(config1.FilePath))
{
File.Copy(config1.FilePath, strUserConfigPath);
}
else
{
string s = Properties.Settings.Default.LastLoadedImage;
var doc = new XDocument();
var declaration = new XDeclaration("1.0", "utf-8", "true");
var config = new XElement(CONFIG);
var userSettings = new XElement(USER_SETTINGS);
var group = new XElement(typeof(Properties.Settings).FullName);
userSettings.Add(group);
config.Add(userSettings);
doc.Add(config);
doc.Declaration = declaration;
doc.Save(strUserConfigPath);
}
}
/// <summary>
/// Saves the in memory dictionary to the user config file
/// </summary>
private void SaveValuesToFile()
{
//load the current xml from the file.
var import = XDocument.Load(UserConfigPath);
//get the settings group (e.g. <Company.Project.Desktop.Settings>)
var settingsSection = import.Element(CONFIG).Element(USER_SETTINGS).Element(typeof(Properties.Settings).FullName);
//iterate though the dictionary, either updating the value or adding the new setting.
foreach(var entry in SettingsDictionary)
{
var setting = settingsSection.Elements().FirstOrDefault(e => e.Attribute(NAME).Value == entry.Key);
if(setting == null) //this can happen if a new setting is added via the .settings designer.
{
var newSetting = new XElement(SETTING);
newSetting.Add(new XAttribute(NAME, entry.Value.name));
newSetting.Add(new XAttribute(SERIALIZE_AS, entry.Value.serializeAs));
newSetting.Value = (entry.Value.value ?? String.Empty);
settingsSection.Add(newSetting);
}
else //update the value if it exists.
{
setting.Value = (entry.Value.value ?? String.Empty);
}
}
import.Save(UserConfigPath);
}
#region Angelo
private object GetDefaultValue(SettingsProperty setting)
{
if (setting.PropertyType.IsEnum)
return Enum.Parse(setting.PropertyType, setting.DefaultValue.ToString());
// Return the default value if it is set
// Return the default value if it is set
if (setting.DefaultValue != null)
{
System.ComponentModel.TypeConverter tc = System.ComponentModel.TypeDescriptor.GetConverter(setting.PropertyType);
return tc.ConvertFromString(setting.DefaultValue.ToString());
}
else // If there is no default value return the default object
{
return Activator.CreateInstance(setting.PropertyType);
}
}
#endregion
}
}
To read properties that are serialized as XML, you first need to deserialize them.
Probably the easiest way is to add a new method called something like getPropertyValue, which determines whether it should return the string value directly or deserialize it first. Then, in your code shown below, you can just call this method instead of using Convert.ChangeType to set property values:
var t = Type.GetType(setting.PropertyType.FullName);
if (SettingsDictionary.ContainsKey(setting.Name))
{
value.SerializedValue = SettingsDictionary[setting.Name].value;
// value.PropertyValue = Convert.ChangeType(SettingsDictionary[setting.Name].value, t);
value.PropertyValue = getPropertyValue(SettingsDictionary[setting.Name].value, t, setting.SerializeAs);
}
else //use defaults in the case where there are no settings yet
{
value.SerializedValue = setting.DefaultValue;
// value.PropertyValue = Convert.ChangeType(setting.DefaultValue, t);
value.PropertyValue = getPropertyValue((string)setting.DefaultValue, t, setting.SerializeAs);
}
An example of how your new method getPropertyValue might work:
private object getPropertyValue(string settingValue, Type settingType, SettingsSerializeAs serializeAs)
{
switch (serializeAs)
{
case SettingsSerializeAs.String:
return settingValue;
case SettingsSerializeAs.Xml:
//for demo purposes, assumes this is your int array--otherwise do further checking to get the correct type
XmlSerializer serializer = new XmlSerializer(typeof(int[]));
return serializer.Deserialize(new StringReader(settingValue));
//implement further types as required
default:
throw new NotImplementedException(string.Format("Settings deserialization as {0} is not implemented", serializeAs));
}
}
This will resolve the invalid cast error and load your array of integers into the setting.
You'll need to apply the corresponding treatment when you save the settings. If you hit complications with that, I suggest you post a new question as the issues are somewhat different.