Setup:
I build a lot of MVC apps from scratch, but that use existing databases.
Using the Entity Framework -> Reverse Engineer Code First context menu item, I get the Code First classes, DbContext class and the mapping classes from the database.
My Requirement:
However, I would also like to generate MetaData classes, so that I can add my customised DisplayName attributes, etc.
The MetaData Classes would be in a different directory (MetaData), so that they don't clutter up the Models directory.
Question:
Anyone know of a T4 Template that does this? It would be odd if I am the first person with this requirement...
My Capabilities:
I am new to T4, but any template that gets files from a given directory, reads each one in a loop, amends it a bit (ideally, adding an attribute to a property!), then writes to a new file in a different directory would be fine, as from there on in, I can figure out how to do it for my specific purpose.
Note:
I don't want the files generated at the same time as the Reverse Engineered Code First files as I don't want to overwrite my MetaData classes. To avoid doing this when I DO run the template, I would amend/write the template so that if the file already exists in the MetaData directory, the template skips that entity and a new MetaData file is not created to overwrite the existing one.
I have seen stuff for Model First and Database First, but not code first. I suppose I could adapt one of those to code first, just replacing the EDMX bits with reading in the previously generated files, getting the properties and adding the DisplayName attribute to them.
Hope this makes sense?
EDIT (Deleted):
I deleted the first edit as I have made progress. See EDIT 2 below.
EDIT 2:
Have also deleted EDIT 2 as I have solved all my problems. See my answer below.
I have been able to solve my problem using blood, sweat, tears and Tangible T4's TemplateFileManagerV2.1.ttinclude and their VisualStudioAutomationHelper.ttinclude, albeit with the modification suggested by Tangible T4 support in the following post:
Tangible T4 support advice for editing their Visual Studio Automation Helper to allow creating files that are not wrapped in a .txt4 file
As I don't have the Pro edition of Tangible T4 it was a bit painful. Hey ho, I'm not looking gift horses in the mouth.
The only outstanding problem is that I can't detect whether a property in the source file is virtual, so I get the navigation properties in my buddy metadata classes as well, which I didn't want. I'll live to fight that one another day.
Also, I can create the files, but they are not included in the project. The code to include them is simple, but I couldn't get it work in the same file, so had to split it out into separate files as follows:
T4_1_GenerateCodeFirstBuddies.tt
T4_2_GenerateCodeFirstBuddies.tt
This separation has a collateral benefit in that T4_1_GenerateCodeFirstBuddies.tt uses the two Tangible T4 helper .ttincludes, one of which leaves a residual error. Running my second file removes the error and the red wavy lines in the solution explorer, which I find really distracting.
So, the code for my files is as follows:
T4_1_GenerateCodeFirstBuddies.tt
<## template debug="true" hostSpecific="true" language="C#" #>
<## output extension=".cs" #>
<## Assembly Name="System.Core" #>
<## import namespace="System" #>
<## import namespace="System.IO" #>
<## import namespace="System.Diagnostics" #>
<## import namespace="System.Linq" #>
<## import namespace="System.Collections" #>
<## import namespace="System.Collections.Generic" #>
<## import namespace="System.Text.RegularExpressions" #>
<## import namespace="EnvDTE" #>
<## include file="VisualStudioAutomationHelper.ttinclude" #>
<## include file="TemplateFileManagerV2.1.ttinclude" #><#
var modelFileDirectory = this.Host.ResolvePath("Models");
var metaDataFilesDirectory = this.Host.ResolvePath("MetaData");
var nspace = "";
var manager = TemplateFileManager.Create(this);
foreach(var file in System.IO.Directory.GetFiles(modelFileDirectory, "*.cs"))
{
var projectItem = this.VisualStudioHelper.FindProjectItem(file);
foreach(EnvDTE.CodeClass classInFile in this.VisualStudioHelper.CodeModel.GetAllCodeElementsOfType(projectItem.FileCodeModel.CodeElements, EnvDTE.vsCMElement.vsCMElementClass, false))
{
var name = classInFile.Name;
if(nspace == "") nspace = classInFile.Namespace.Name;
// Danger: Beware if a table name includes the string "Context" or "AspNet"!!
// These files are removed because they are either the DbContext, or the sysdiagram file, or else the AspNet.Identity tables
if(name != "sysdiagram" && name.IndexOf("Context") == -1 && name.IndexOf("AspNet") == -1)
{
if(!FileExists(metaDataFilesDirectory, classInFile.Name + "MetaData.cs"))
{
manager.StartNewFile(name +"MetaData.cs", "", "MetaData"); #>
using System;
using System.Collections.Generic;
using System.ComponentModel;
//using System.ComponentModel.DataAnnotations;
//using Wingspan.Web.Mvc.Extensions;
using Wingspan.Web.Mvc.Crud;
namespace <#= nspace #>
{
public class <#= name + "MetaData" #>
{
<# foreach (CodeElement mem in classInFile.Members)
{
if (mem.Kind == vsCMElement.vsCMElementProperty) // && "[condition to show that mem is not marked as virtual]")
{
PushIndent(" ");
WriteLineDisplayName(mem);
WriteLineProperty(mem);
WriteLine("");
PopIndent();
}
} #>
}
public partial class <#= name #> : IInjectItemSL
{
public ItemSL ItemSL
{
get
{
return new ItemSL
{
ItemId = <#= name #>Id, ItemText = Name
};
}
}
}
}<#
}
}
}
}
manager.Process();
#>
<#+
// Check for file existence
bool FileExists(string directory, string filename)
{
return File.Exists(Path.Combine(directory, filename));
}
// Get current folder directory
string GetCurrentDirectory()
{
return System.IO.Path.GetDirectoryName(Host.TemplateFile);
}
string GetRootDirectory()
{
return this.Host.ResolvePath("");
}
// Get content of file name
string xOutputFile(string filename)
{
using(StreamReader sr =
new StreamReader(Path.Combine(GetCurrentDirectory(),filename)))
{
return sr.ReadToEnd();
}
}
// Get friendly name for property names
string GetFriendlyName(string value)
{
return Regex.Replace(value,
"([A-Z]+)", " $1",
RegexOptions.Compiled).Trim();
}
void WriteLineProperty(CodeElement ce)
{
var access = ((CodeProperty) ce).Access == vsCMAccess.vsCMAccessPublic ? "public" : "";
WriteLine(access + " " + (((CodeProperty) ce).Type).AsFullName + " " + ce.Name + " { get; set; }");
}
void WriteLineDisplayName(CodeElement ce)
{
var name = ce.Name;
if (!string.IsNullOrEmpty(name))
{
name = GetFriendlyName(name);
WriteLine(string.Format("[DisplayName(\"{0}\")]", name));
}
}
#>
T4_2_GenerateCodeFirstBuddies.tt:
<## template debug="true" hostSpecific="true" language="C#" #>
<## output extension=".cs" #>
<## Assembly Name="System.Core" #>
<## import namespace="System" #>
<## import namespace="System.IO" #>
<## import namespace="System.Diagnostics" #>
<## import namespace="System.Linq" #>
<## import namespace="System.Collections" #>
<## import namespace="System.Collections.Generic" #>
<## import namespace="System.Text.RegularExpressions" #>
<## import namespace="EnvDTE" #>
<## include file="VisualStudioAutomationHelper.ttinclude" #>
<## include file="TemplateFileManagerV2.1.ttinclude" #><#
var metaDataFilesDirectory = this.Host.ResolvePath("MetaData");
var metaDataFiles = System.IO.Directory.GetFiles(metaDataFilesDirectory, "*.cs");
var project = VisualStudioHelper.CurrentProject;
var projectItems = project.ProjectItems;
foreach( var f in metaDataFiles)
{
projectItems.AddFromFile(f);
}
#>
The output files generated are good enough for me, and look along the lines of:
using System;
using System.Collections.Generic;
using System.ComponentModel;
//using System.ComponentModel.DataAnnotations;
//using Wingspan.Web.Mvc.Extensions;
using Wingspan.Web.Mvc.Crud;
namespace BuddyClassGenerator.Models
{
public class ChemicalMetaData
{
[DisplayName("Chemical Id")]
public System.Guid ChemicalId { get; set; }
[DisplayName("Active Ingredient")]
public System.String ActiveIngredient { get; set; }
[DisplayName("Type")]
public System.String Type { get; set; }
[DisplayName("LERAP")]
public System.String LERAP { get; set; }
[DisplayName("Hazard Classification")]
public System.String HazardClassification { get; set; }
[DisplayName("MAPP")]
public System.Int32 MAPP { get; set; }
[DisplayName("Hygiene Practice")]
public System.String HygienePractice { get; set; }
[DisplayName("Medical Advice")]
public System.String MedicalAdvice { get; set; }
[DisplayName("Label")]
public System.String Label { get; set; }
[DisplayName("PPE")]
public System.String PPE { get; set; }
[DisplayName("Warnings")]
public System.String Warnings { get; set; }
[DisplayName("Products")]
public System.Collections.Generic.ICollection<BuddyClassGenerator.Models.Product> Products { get; set; }
}
public partial class Chemical : IInjectItemSL
{
public ItemSL ItemSL
{
get
{
return new ItemSL
{
ItemId = ChemicalId, ItemText = Name
};
}
}
}
You will no doubt note that I have put two classes in the same file. Might not be best practice but it saves me time and visual clutter in the folders, so it is my privilege.
To do list: 1, not include the navigation properties in the buddy class; 2, remove the namespace names from the property types.
I hope this helps someone, but do remember that to get it to work you will need the Tangible T4 ttincludes detailed above.
Related
Cannot work with more than one table. What to do to get to work with two, three or more tables?
Visual Studio >>> Xamarin-Forms
/// I think maybe this code needs to be fixed somehow.
/// this code is placed in App.cs (up)
static Data.TableTwo tabletwo;
static Data.TableOne tableone;
/// crashed
public Task<int> SaveItemAsync(TableTwo item)
{
if (item.ID != 0)
{
return tabletwo.UpdateAsync(item);
}
else
{
return tabletwo.InsertAsync(item);
}
}
/// ***I think maybe this code needs to be fixed somehow.
/// this code is placed in App.cs (down below)
public static Data.TableTwo tabletwo
{
get
{
if (datadistance == null)
{
tabletwo = new Data.TableTwo(Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData), "TodoSQLite.db3"));
}
return tabletwo;
}
}
/// ***I think maybe this code needs to be fixed somehow.
/// this code is placed in App.cs (down below)
public static Data.TableOne tableone
{
get
{
if (tableone == null)
{
tableone = new Data.TableOne(Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData), "TodoSQLite.db3"));
}
return tableone;
}
}
above code works correctly. When the code above is called. Application falls.
I have two tables. With one table when the user works (saves and deletes data), then everything works. If the user starts working with another table (save data) the application crashes.
!!!application tree!!!
DataBase(folder)
TableTwo(file)
TableOne(file)
Models(folder)
TableTwo(file)
TableOne(file)
Everything was done according to the code from the article https://learn.microsoft.com/en-us/xamarin/xamarin-forms/app-fundamentals/databases#using-sqlite
In fact, I just copied the code a second time, and pasted into the project. - this is what I have done creating the second table and working with it (deleting insert data)
Let's assume your two tables are of types Load and Category and your database is of type MyDatabase
You might want to keep a single connection to SqlLite inside the MyDatabase class and add the methods to interact with your tables as follows:
public class MyDatabase
{
private readonly SQLiteAsyncConnection _connection;
public MyDatabase():this(Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData), "MyDatabase.db3"))
{
}
internal MyDatabase(string dbPath)
{
_connection = new SQLiteAsyncConnection(dbPath);
_connection.CreateTableAsync<Load>().Wait();
_connection.CreateTableAsync<Category>().Wait();
}
public Task<List<Load>> GetLoads() =>
_connection.Table<Load>().ToListAsync();
public Task<List<Category>> GetCategories() =>
_connection.Table<Category>().ToListAsync();
public Task<Load> GetLoad(int id) =>
_connection.Table<Load>().Where(i => i.Id == id).FirstOrDefaultAsync();
public Task<int> SaveLoad(Load item) =>
item.Id != 0 ? _connection.UpdateAsync(item) : _connection.InsertAsync(item);
public Task<int> DeleteLoad(Load item) =>
_connection.DeleteAsync(item);
}
Here is a good sample: https://github.com/xamarin/xamarin-forms-samples/blob/master/Todo/Todo/Data/TodoItemDatabase.cs, but it contains a single table 😊
When I try to index a doc of my defined type, having a list which is supposed to be mapped as a nested-object ("type":"nested"), it's getting mapped as a regular object type.
Take a look at the code:
I've got a simple class like this one:
[ElasticType()]
public class MyJob
{
[ValueFieldAttribute]
public int jobCode { get; set; }
[ValueFieldAttribute(Type = FieldType.nested)]
public IList<JobProfessionalFieldInfo> JobProfessionalFields { get; set; }
}
The code for the JobProfessionalFieldInfo class is:
[ElasticType()]
public class JobProfessionalFieldInfo
{
[ValueFieldAttribute]
public int JobId { get; set; }
[ValueFieldAttribute]
public int CategoryId { get; set; }
}
The code for the ValueFieldAttribute class is:
public class ValueFieldAttribute : ElasticPropertyAttribute
{
public ValueFieldAttribute()
: base()
{
this.Store = false;
this.Index = FieldIndexOption.not_analyzed;
}
}
My program:
static void Main(string[] args)
{
ConnectionSettings node = new ConnectionSettings(new Uri("http://localhost:9200"));
node.SetDefaultIndex("jobs");
ElasticClient client = new ElasticClient(node);
List<JobProfessionalFieldInfo> list = new List<JobProfessionalFieldInfo>();
list.Add(new JobProfessionalFieldInfo { CategoryId = 1, JobId = 1 });
list.Add(new JobProfessionalFieldInfo { CategoryId = 2, JobId = 2 });
var res = client.Index<MyJob>(new MyJob
{
jobCode = 1,
JobProfessionalFields = list
},"jobs", "MyJob",1);
}
Now, when I run it, it indexes the object successfully... BUT(!!) when I get the mapping of the index with GET jobs/MyJob/_mapping, I see that jobProfessionalFields has no "type":"nested" in its mapping.
That results in a query like the following one, returning the indexed doc while it's not supposed to get it back (that's what nested-type is for right?..):
GET jobs/_search
{
"query":
{
"bool":
{
"must":
[
{"match": {"jobId":1}},
{"match": {"categoryId":2}}
]
}
}
}
It's not the end:
I'd a look at here,
there the guy that answered tells that when we use annotations we need to manually call the createIndex and Map methods, but the problem is that I don't have any generic Map method...!
Take a look at here: (just to make you get into the link - here's its start..)
namespace Nest
{
public partial class ElasticClient...
And I don't know how to use the non-generic Map method to put the mapping of my MyJob class.
How can I cause this stuff to map the jobProfessionalFields as nested-type dudes?
Thanks for any help of you guys!
OK, got it LOL!
The MapFromAttributes<> is the right generic method for putting the mapping (at least in the current Nest version I'm using - 0.12.0).
But it demands a manual call for the index creationg, o.w it gives an IndexMissing exception (like the guy in the above mentioned link said).
client.CreateIndex("jobs", new IndexSettings { });
var res = client.MapFromAttributes<MyJob>("jobs","MyJob");
But that's really interesting why isn't it enough to just define the
[ElasticProperty(Type = FieldType.nested)],
in order to get the nested mapping though..
I would be glad to get an answer for that one.
I am trying to use Rome for parsing some rss feeds. One of the rss feeds says
specifies 0.91 as the version and no custom xml namespace is defined but the entries still have a custom element in them. Can I use Rome to parse such custom tags without any defined namespace?
Thanks.
Yes. You need to write a custom parser to do it.
Let's say you want to handle the elements customString and customDate. Start by extending the Item class to store the custom elements.
package com.example;
import com.sun.syndication.feed.rss.Item;
import java.util.Date;
public class CustomItem extends Item {
private String _customString;
private Date _customDate;
public String getCustomString() {
return _customString;
}
public void setCustomString(String customString) {
_customString = customString;
}
public Date getCustomDate() {
return _customDate;
}
public void setCustomDate(Date customDate) {
_customDate = customDate;
}
}
Then write the parser. You also need to handle any standard elements you want to parse.
package com.example;
import com.example.CustomItem;
import com.sun.syndication.feed.rss.Item;
import com.sun.syndication.io.WireFeedParser;
import com.sun.syndication.io.impl.DateParser;
import com.sun.syndication.io.impl.RSS091UserlandParser;
import org.jdom.Element;
public class CustomParser extends RSS091UserlandParser implements WireFeedParser {
public CustomItem parseItem(Element rssRoot, Element eItem) {
CustomItem customItem = new CustomItem();
// Standard elements
Item standardItem = super.parseItem(rssRoot, eItem);
customItem.setTitle(standardItem.getTitle());
customItem.setDescription(standardItem.getDescription());
// Non-standard elements
Element e = eItem.getChild("customString", getRSSNamespace());
if (e != null) {
customItem.setCustomString(e.getText());
}
e = eItem.getChild("customDate", getRSSNamespace());
if (e != null) {
customItem.setCustomDate(DateParser.parseDate(e.getText()));
}
return customItem;
}
}
Finally you need to define your parser in a rome.properties file along with parsers for any other type of feed you want to handle.
# Feed Parser implementation classes
#
WireFeedParser.classes=com.example.CustomParser
You need to write a custom converter to get the data.
part of code same to above.
Start by extending the Item class to store the custom elements.
package com.example;
import com.sun.syndication.feed.rss.Item;
import java.util.Date;
public class CustomItem extends Item {
private String _customString;
private Date _customDate;
public String getCustomString() {
return _customString;
}
public void setCustomString(String customString) {
_customString = customString;
}
public Date getCustomDate() {
return _customDate;
}
public void setCustomDate(Date customDate) {
_customDate = customDate;
}
}
Then write the parser. You also need to handle any standard elements you want to parse.
package com.example;
import com.example.CustomItem;
import com.sun.syndication.feed.rss.Item;
import com.sun.syndication.io.WireFeedParser;
import com.sun.syndication.io.impl.DateParser;
import com.sun.syndication.io.impl.RSS091UserlandParser;
import org.jdom.Element;
public class CustomParser extends RSS091UserlandParser implements WireFeedParser {
public CustomItem parseItem(Element rssRoot, Element eItem) {
CustomItem customItem = new CustomItem();
// Standard elements
Item standardItem = super.parseItem(rssRoot, eItem);
customItem.setTitle(standardItem.getTitle());
customItem.setDescription(standardItem.getDescription());
// Non-standard elements
Element e = eItem.getChild("customString", getRSSNamespace());
if (e != null) {
customItem.setCustomString(e.getText());
}
e = eItem.getChild("customDate", getRSSNamespace());
if (e != null) {
customItem.setCustomDate(DateParser.parseDate(e.getText()));
}
return customItem;
}
}
Then write the converter.
public class CustomConverter extends ConverterForRSS20 {
protected SyndEntry createSyndEntry(Item item) {
List<HashMap<String,String>> temp = new ArrayList<HashMap<String,String>>();
SyndEntry syndEntry = super.createSyndEntry(item);
customItem customItem = (customItem)item;
List<String> customList = new ArrayList<String>();
customList.add( customItem.getCustomString() );
//set to empty attribute ex foreignmarkup
syndEntry.setForeignMarkup( customList );
return syndEntry;
}
}
Finally you need to define your parser in a rome.properties file along with parsers for any other type of feed you want to handle.
# Feed Parser implementation classes
#
WireFeedParser.classes=com.example.CustomParser
# Feed Converter implementation classes
#
Converter.classes=com.example.CustomConverter
Then you can get value.
SyndFeed feed = input.build(new XmlReader(feedUrl));
List<SyndEntryImpl> entrys = feed.getEntries();
for(SyndEntryImpl entry:entrys ){
System.out.println( entry.getForeignMarkup() );
}
I have this class
public class Contact
{
public int Id { get; set; }
public string ContaSurname { get; set; }
public string ContaFirstname { get; set; }
// and other properties...
}
And I want to create a form that allo me to edit all those fields. So I used this code
<h2>Contact Record</h2>
#Html.EditorFor(c => Model.Contact)
This works fine, but I want to customize how the elements are displayed. For instance I want each field to be displayed in the same line as its label. Because now, the generated html is like this :
<div class="editor-label">
<label for="Contact_ContaId">ContaId</label>
</div>
<div class="editor-field">
<input id="Contact_ContaId" class="text-box single-line" type="text" value="108" name="Contact.ContaId">
</div>
I agree to the solution of jrummell above:
When you use the EditorFor-Extension, you have to write a custom
editor template to describe the visual components.
In some cases, I think it is a bit stiff to use an editor template for
several model properties with the same datatype. In my case, I want to use decimal currency values in my model which should be displayed as a formatted string. I want to style these properties using corresponding CSS classes in my views.
I have seen other implementations, where the HTML-Parameters have been appended to the properties using annotations in the Model. This is bad in my opinion, because view information, like CSS definitions should be set in the view and not in a data model.
Therefore I'm working on another solution:
My model contains a decimal? property, which I want to use as a currency field.
The Problem is, that I want to use the datatype decimal? in the model, but display
the decimal value in the view as formatted string using a format mask (e.g. "42,13 €").
Here is my model definition:
[DataType(DataType.Currency), DisplayFormat(DataFormatString = "{0:C2}", ApplyFormatInEditMode = true)]
public decimal? Price { get; set; }
Format mask 0:C2 formats the decimal with 2 decimal places. The ApplyFormatInEditMode is important,
if you want to use this property to fill a editable textfield in the view. So I set it to true, because in my case I want to put it into a textfield.
Normally you have to use the EditorFor-Extension in the view like this:
<%: Html.EditorFor(x => x.Price) %>
The Problem:
I cannot append CSS classes here, as I can do it using Html.TextBoxFor for example.
To provide own CSS classes (or other HTML attributes, like tabindex, or readonly) with the EditorFor-Extension is to write an custom HTML-Helper,
like Html.CurrencyEditorFor. Here is the implementation:
public static MvcHtmlString CurrencyEditorFor<TModel, TValue>(this HtmlHelper<TModel> html, Expression<Func<TModel, TValue>> expression, Object htmlAttributes)
{
TagBuilder tb = new TagBuilder("input");
// We invoke the original EditorFor-Helper
MvcHtmlString baseHtml = EditorExtensions.EditorFor<TModel, TValue>(html, expression);
// Parse the HTML base string, to refurbish the CSS classes
string basestring = baseHtml.ToHtmlString();
HtmlDocument document = new HtmlDocument();
document.LoadHtml(basestring);
HtmlAttributeCollection originalAttributes = document.DocumentNode.FirstChild.Attributes;
foreach(HtmlAttribute attr in originalAttributes) {
if(attr.Name != "class") {
tb.MergeAttribute(attr.Name, attr.Value);
}
}
// Add the HTML attributes and CSS class from the View
IDictionary<string, object> additionalAttributes = (IDictionary<string, object>) HtmlHelper.AnonymousObjectToHtmlAttributes(htmlAttributes);
foreach(KeyValuePair<string, object> attribute in additionalAttributes) {
if(attribute.Key == "class") {
tb.AddCssClass(attribute.Value.ToString());
} else {
tb.MergeAttribute(attribute.Key, attribute.Value.ToString());
}
}
return MvcHtmlString.Create(HttpUtility.HtmlDecode(tb.ToString(TagRenderMode.SelfClosing)));
}
The idea is to use the original EditorFor-Extension to produce the HTML-Code and to parse this HTML output string to replace the created
CSS Html-Attribute with our own CSS classes and append other additional HTML attributes. For the HTML parsing, I use the HtmlAgilityPack (use google).
In the View you can use this helper like this (don't forget to put the corresponding namespace into the web.config in your view-directory!):
<%: Html.CurrencyEditorFor(x => x.Price, new { #class = "mypricestyles", #readonly = "readonly", #tabindex = "-1" }) %>
Using this helper, your currency value should be displayed well in the view.
If you want to post your view (form), then normally all model properties will be sent to your controller's action method.
In our case a string formatted decimal value will be submitted, which will be processed by the ASP.NET MVC internal model binding class.
Because this model binder expects a decimal?-value, but gets a string formatted value, an exception will be thrown. So we have to
convert the formatted string back to it's decimal? - representation. Therefore an own ModelBinder-Implementation is necessary, which
converts currency decimal values back to default decimal values ("42,13 €" => "42.13").
Here is an implementation of such a model binder:
public class DecimalModelBinder : IModelBinder
{
public object BindModel(ControllerContext controllerContext, ModelBindingContext bindingContext)
{
object o = null;
decimal value;
var valueResult = bindingContext.ValueProvider.GetValue(bindingContext.ModelName);
var modelState = new ModelState { Value = valueResult };
try {
if(bindingContext.ModelMetadata.DataTypeName == DataType.Currency.ToString()) {
if(decimal.TryParse(valueResult.AttemptedValue, NumberStyles.Currency, null, out value)) {
o = value;
}
} else {
o = Convert.ToDecimal(valueResult.AttemptedValue, CultureInfo.CurrentCulture);
}
} catch(FormatException e) {
modelState.Errors.Add(e);
}
bindingContext.ModelState.Add(bindingContext.ModelName, modelState);
return o;
}
}
The binder has to be registered in the global.asax file of your application:
protected void Application_Start()
{
...
ModelBinders.Binders.Add(typeof(decimal), new DecimalModelBinder());
ModelBinders.Binders.Add(typeof(decimal?), new DecimalModelBinder());
...
}
Maybe the solution will help someone.
Create a partial view called Contact.cshtml with your custom markup in Views/Shared/EditorTemplates. This will override the default editor.
As noted by #smartcavemen, see Brad Wilson's blog for an introduction to templates.
I'm trying to unit test some code that calls into VirtualPathUtility.ToAbsolute.
Is this possible with the unit testing tools provided with VS 2008? If not, is it possible with a later version of Visual Studio?
We're well past VS 2008 but for anyone who is still grappling with this issue, I've found a solution on: http://forums.asp.net/t/995143.aspx?Mocking+HTTPContext+object.
Use the following code in your test init to override the default AppDomain values. (The VirutalPathUtility static methods will use your new values.)
[TestInitialize]
public void Initialize()
{
// Fake out env for VirtualPathUtility.ToAbsolute(..)
string path = AppDomain.CurrentDomain.BaseDirectory;
const string virtualDir = "/";
AppDomain.CurrentDomain.SetData(".appDomain", "*");
AppDomain.CurrentDomain.SetData(".appPath", path);
AppDomain.CurrentDomain.SetData(".appVPath", virtualDir);
AppDomain.CurrentDomain.SetData(".hostingVirtualPath", virtualDir);
AppDomain.CurrentDomain.SetData(".hostingInstallDir", HttpRuntime.AspInstallDirectory);
TextWriter tw = new StringWriter();
HttpWorkerRequest wr = new SimpleWorkerRequest("default.aspx", "", tw);
HttpContext.Current = new HttpContext(wr);
}
Static classes and methods are really hard to work with in unit tests (which is one reason why i try to avoid them). In this case, I would probably develop a wrapper around the static class, containing just those methods that I use. I would then use my wrapper class in place of the real class. The wrapper class would be constructed so that it is easy to mock out.
Example (sort of) using RhinoMocks. Note that it uses dependency injection to give the class under test a copy of the wrapper. If the supplied wrapper is null, it creates one.
public class MyClass
{
private VPU_Wrapper VPU { get; set; }
public MyClass() : this(null) {}
public MyClass( VPU_Wrapper vpu )
{
this.VPU = vpu ?? new VPU_Wrapper();
}
public string SomeMethod( string path )
{
return this.VPU.ToAbsolute( path );
}
}
public class VPU_Wrapper
{
public virtual string ToAbsolute( string path )
{
return VirtualPathUtility.ToAbsolute( path );
}
}
[TestMethod]
public void SomeTest()
{
string path = "~/path";
string expected = "/app/path";
var vpu = MockRepository.GenerateMock<VPU_Wrapper>();
vpu.Expect( v => v.ToAbsolute( path) ).Return( expected );
MyClass class = new MyClass( vpu );
string actual = class.SomeMethod( path );
Assert.AreEqual( expected, actual );
vpu.VerifyAllExpectations();
}
Using Microsoft Fakes we can fake VirtualPathUtility ToAbsolute Method easily.
Browse System.Web in References > Right Click > Add Fakes Assembly.
Use Following Code
using Microsoft.QualityTools.Testing.Fakes;
using Microsoft.VisualStudio.TestTools.UnitTesting;
using System.Web.Fakes;
public class TestCode
{
[TestMethod]
public void TestSummaryTabLinks()
{
using (ShimsContext.Create())
{
//Fake VirtualPathUtility ToAbsolute method to Work properly in case of Unit Test Project
//For Finding Relative url.
ShimVirtualPathUtility.ToAbsoluteString = (string s) => { return s; };
MyClass class = new MyClass( vpu );
string actual = class.SomeMethod( path );
Assert.AreEqual( expected, actual );
}
}
}