I am trying to create a custom LabelFor helper to apply by default instead of the standard LabelFor helper include in System.Web.Mvc.Html. I want my LabelFor to take model properties that are PascalCase and make a label that appears as multiple words. For example the property FirstName would appear as "First Name".
I found this post that shows how to make a custom LabelFor helper that allows the addition of html attributes.
I recreated this helper in VB.Net and modified it to do what I want, but I am not able to get it to work.
Imports System.Linq.Expressions
Imports System.Runtime.CompilerServices
Public Module LabelExtensions
<Extension()> _
Public Function LabelFor(Of Tmodel, TValue)(html As HtmlHelper(Of Tmodel), expression As Expression(Of Func(Of Tmodel, TValue))) As MvcHtmlString
Dim metadata As ModelMetadata = ModelMetadata.FromLambdaExpression(expression, html.ViewData)
Dim htmlFieldName As String = ExpressionHelper.GetExpressionText(expression)
Dim labelText As String = If(metadata.DisplayName, If(metadata.PropertyName, htmlFieldName.Split(".").Last()))
If String.IsNullOrEmpty(labelText) Then Return MvcHtmlString.Empty
labelText = Regex.Replace(labelText, ".[A-Z]", Function(m) m.ToString()(0) & " " & Char.ToLower(m.ToString()(1)))
Dim tag As TagBuilder = New TagBuilder("label")
tag.Attributes.Add("for", html.ViewContext.ViewData.TemplateInfo.GetFullHtmlFieldId(htmlFieldName))
tag.SetInnerText(labelText)
Return MvcHtmlString.Create(tag.ToString(TagRenderMode.Normal))
End Function
The sample includes this Module in Namespace System.Web.Mvc.Html but when I add the namespace declaration to this module everything in the rest of the project goes haywire. For example, each of my models has a Primary Key property that is a Guid datatype and as soon as I add the namespace above to the module, I get several errors stating that System.Guid is not defined among other similar errors.
I've tried to import my project namespace into a view in order to use the custom helper, but then I get an error that says Overload resolution failed because no 'LabelFor' is most specific for these arguments.
I'm stuck. Can anybody help?
I am trying to avoid having to specify a DisplayName for every PascalCase property I have in many models.
I was able to do this successfully if I named my extension method something other than LabelFor. When I named it LabelForPascalCase I was able to use that function in my views. I think the extension method was not overloading the existing method because they had the same signature.
Related
I'm attempting to override the default behavior of the SQL migrations generator so that I may specify a custom foreign key constraint name, as discussed here. I've wired up the configuration as advised.
Unfortunately, however, it's not going so well.
A quick logging statement reveals that the GetFkName() function is never hit.
I tried an alternate configuration construct, as discussed here and here, but I'm getting this error when I attempt to generate a migration:
More than one migrations configuration type was found in the assembly 'ConsoleApp1'. Specify the name of the one to use.
I find this result a bit odd, as I have only one configuration class, one SQL generation class, and one context class (the code below doesn't reflect this, but I commented out the extras for my actual tests). Specifying the configuration type on the command line, as indicated here, errors with this:
System.InvalidOperationException: The type 'ConsoleApp1.Db.CustomDbConfiguration2' does not inherit from 'System.Data.Entity.DbConfiguration'. Entity Framework code-based configuration classes must inherit from 'System.Data.Entity.DbConfiguration'.
All of this brings us back here, then, which doesn't work for the aforementioned reason (GetFkName() never gets hit). So it seems I'm chasing my tail (didn't know I had one until today).
What should I do to get this override to work correctly?
Configuration
Imports System.Data.Entity
Imports System.Data.Entity.Migrations
Imports System.Data.Entity.SqlServer
Namespace Db
Friend Class CustomDbConfiguration
Inherits DbConfiguration
Public Sub New()
Me.SetMigrationSqlGenerator(SqlProviderServices.ProviderInvariantName, Function() New CustomSqlGenerator)
End Sub
End Class
Friend Class CustomDbConfiguration2
Inherits DbMigrationsConfiguration(Of Context)
Public Sub New()
Me.SetSqlGenerator(SqlProviderServices.ProviderInvariantName, New CustomSqlGenerator2(Me.GetSqlGenerator(SqlProviderServices.ProviderInvariantName)))
Me.ContextType = GetType(Context)
End Sub
End Class
End Namespace
SQL Generator
Imports System.Data.Entity.Migrations.Model
Imports System.Data.Entity.Migrations.Sql
Imports System.Data.Entity.SqlServer
Namespace Db
Friend Class CustomSqlGenerator
Inherits SqlServerMigrationSqlGenerator
Protected Overrides Sub Generate(AddForeignKeyOperation As AddForeignKeyOperation)
AddForeignKeyOperation.Name = GetFkName(AddForeignKeyOperation.PrincipalTable, AddForeignKeyOperation.DependentTable, AddForeignKeyOperation.DependentColumns.ToArray())
MyBase.Generate(AddForeignKeyOperation)
End Sub
Protected Overrides Sub Generate(DropForeignKeyOperation As DropForeignKeyOperation)
DropForeignKeyOperation.Name = GetFkName(DropForeignKeyOperation.PrincipalTable, DropForeignKeyOperation.DependentTable, DropForeignKeyOperation.DependentColumns.ToArray())
MyBase.Generate(DropForeignKeyOperation)
End Sub
Private Shared Function GetFkName(PrimaryKeyTable As String, ForeignKeyTable As String, ParamArray ForeignTableFields As String()) As String
IO.File.WriteAllText("D:\Logs\FkNameTest.log", $"{Now.ToString}{vbCrLf}")
Return $"FK_{ForeignKeyTable}_{PrimaryKeyTable}"
End Function
End Class
Friend Class CustomSqlGenerator2
Inherits MigrationSqlGenerator
Public Sub New(Generator As MigrationSqlGenerator)
Me.Generator = Generator
End Sub
Public Overrides Function Generate(MigrationOperations As IEnumerable(Of MigrationOperation), ProviderManifestToken As String) As IEnumerable(Of MigrationStatement)
Return Me.Generator.Generate(MigrationOperations, ProviderManifestToken)
End Function
Private ReadOnly Generator As MigrationSqlGenerator
End Class
End Namespace
Context
Imports System.Data.Common
Imports System.Data.Entity
Imports System.Data.SqlClient
Imports System.Reflection
Namespace Db
<DbConfigurationType(GetType(CustomDbConfiguration2))>
Friend Class Context
Inherits DbContext
Public Sub New()
MyBase.New(DbConnection.ConnectionString)
End Sub
Private Sub New(Connection As DbConnection)
MyBase.New(Connection, True)
Database.SetInitializer(New CreateDatabaseIfNotExists(Of Context))
Database.SetInitializer(New MigrateDatabaseToLatestVersion(Of Context, Migrations.Configuration))
Me.Database.Initialize(False)
End Sub
Public Shared Function Create() As Context
Return New Context(DbConnection)
End Function
Private Shared ReadOnly Property DbConnection As SqlConnection
Get
Return New SqlConnection(Utils.DbConnectionString)
End Get
End Property
Protected Overrides Sub OnModelCreating(Builder As DbModelBuilder)
Builder.Configurations.AddFromAssembly(Assembly.GetExecutingAssembly)
MyBase.OnModelCreating(Builder)
End Sub
Public Property Documents As DbSet(Of Document)
Public Property Sections As DbSet(Of Section)
End Class
End Namespace
Disclaimer: I haven't coded in VB for many years, these code examples are my feeble attempt to translate my working example in C# into OPs native VB. Please feel free to update my syntax ;)
You can manually edit the migration scripts to specify a custom name for each ForeignKey by specifying a value for the optional Name parameter in the call to Tablebuilder.ForeignKey as part of a create table statement:
CreateTable(
"dbo.CorporationVariety",
Function(c) New With
{
.Id = c.Int(nullable: false, identity:= true),
.CorporationId = c.Int(nullable:= false),
.VarietyId = c.Int(nullable:= false),
}) _
.PrimaryKey(Function(t) t.Id)
.ForeignKey("dbo.Corporation", Function(t) t.CorporationId, name := "FKCorporatationCorporationVarietyCorporationId")
.ForeignKey("dbo.Variety", Function(t) t.VarietyId, name := "FKVarietyCorporationVarietyVarietyId")
.Index(Function(t) t.CorporationId)
.Index(Function(t) t.VarietyId)
Or as part of a DbMigration.AddForeignKey statement:
AddForeignKey("dbo.CorporationVariety", "CorporationId", "dbo.Corporation", name := "FKCorporatationCorporationVarietyCorporationId")
AddForeignKey("dbo.CorporationVariety", "VarietyId", "dbo.Variety", name := "FKVarietyCorporationVarietyVarietyId")
If you have a lot of keys in your Model, and you want to implement a specific convention, (as in a standard rule or sequence of code that you want to apply in given scenarios) across all keys, then Normally the first place to look for a solution is EF Code First Conventions.
Unfortunately, there is neither a standard convention that can help you here nor can you define a custom name for a foreign key using fluent notation...
Normally we would go ahead and create a Custom Code First Convention to define your custom logic, this works in generally 2 ways:
Your convention executes standard configuration via Fluent Notation
we already noted that this option is not available to us...
Your convention logic stores custom metadata to the model via annotations
Primary and Foreign keys seem to be an anomaly in the EF Code First Runtime, there does not seem to be a way to easily access the annotations from the associations even though they are relatively easy to define.
I was surprised to find this and stumbled across this post that further confirms this: https://stackoverflow.com/a/54369685/1690217
Update I started this post assuming that Conventions was the right way to go, because I use it for many other customisations that I've needed to apply over the years. If you are looking to implement other similar types of customisations, look to Conventions first.
We can still easily override the standard VisualBasicMigrationCodeGenerator that generates the migration code files, so lets jump straight into that.
coapply to custom name for your ForeignKey and then implement a custom MigrationCodeGenerator to process the output from your convention.
Create a custom VisualBasicMigrationCodeGenerator
Register the Code Generator so that it is used by EF to generate the next migration
NOTE: This will not force existing keys in your database to be renamed. To do that you would need to force each key to be dropped and re-added back. For a large model Consider using a T4 template to create custom once-off migration logic to achieve this, once the above steps are in place.
Think of your Custom VisualBasicMigrationCodeGenerator as your personal EF code first sour dough culture, you can share this and re-use it for every new application, adding new functionality and improvements with each iteration. But Conventions are the configuration options that you may not want in every project, (which is why using _Conventions_ for OPs solution was my first direction.)
1. Create a custom VisualBasicMigrationCodeGenerator
Create a new class that inherits from the EF VisualBasicMigrationCodeGenerator, the minimal we need to do is override the AddForeignKeyOperation and modify the Name of the key and call the base implementation. This will affect all new keys added to the model.
To target keys added as part of CreateTable we will have to override GenerateInline(AddForeignKeyOperation...), however the base implemention (in the C# Generator...) doesn't obey the custom Name so instead we have to replace the implementation entirely.
When doing this, goto the EF project on GitHub and start with the original implementation, then inject your customizations as needed.
Please excuse this C#, I didn't have time to translate it, it does generate the correct VB code though ;)
public class CustomVBMigrationCodeGenerator : System.Data.Entity.Migrations.Design.VisualBasicMigrationCodeGenerator
{
protected override void Generate(AddForeignKeyOperation addForeignKeyOperation, IndentedTextWriter writer)
{
ApplyCustomFKName(addForeignKeyOperation);
base.Generate(addForeignKeyOperation, writer);
}
private void ApplyCustomFKName(ForeignKeyOperation operation)
{
// expecting FK without scheme or underscores: "FK{DependentTable}{PrincipalTable}{FKField}"
operation.Name = $"FK{StripSchemeFromName(operation.DependentTable)}{StripSchemeFromName(operation.PrincipalTable)}{String.Join("", operation.DependentColumns)}";
}
private string StripSchemeFromName(string dbObjectName)
{
return dbObjectName.Split(new[] { '.' }, 2).Last();
}
/// <summary>
/// Generates code to perform an <see cref="AddForeignKeyOperation" /> as part of a <see cref="CreateTableOperation" />.
/// </summary>
/// <param name="addForeignKeyOperation"> The operation to generate code for. </param>
/// <param name="writer"> Text writer to add the generated code to. </param>
protected virtual void GenerateInline(AddForeignKeyOperation addForeignKeyOperation, IndentedTextWriter writer)
{
// sourced from https://github.com/aspnet/EntityFramework6/blob/master/src/EntityFramework/Migrations/Design/VisualBasicMigrationCodeGenerator.cs
Check.NotNull(addForeignKeyOperation, "addForeignKeyOperation");
Check.NotNull(writer, "writer");
writer.WriteLine(" _");
writer.Write(".ForeignKey(" + Quote(addForeignKeyOperation.PrincipalTable) + ", ");
Generate(addForeignKeyOperation.DependentColumns, writer);
// Our Custom logic
ApplyCustomFKName(addForeignKeyOperation);
// Insert our custom name if provided
if (!addForeignKeyOperation.HasDefaultName)
{
writer.Write(", name := " + Quote(addForeignKeyOperation.Name));
}
if (addForeignKeyOperation.CascadeDelete)
{
writer.Write(", cascadeDelete := True");
}
writer.Write(")");
}
}
2. Register the Code Generator so that it is used by EF to generate the next migration
Locate Configuration.vb in your project, int the constructor set the CodeGenerator to an instance of your CustomVBMigrationCodeGenerator :
Public Sub New()
AutomaticMigrationsEnabled = false
CodeGenerator = new CustomVBMigrationCodeGenerator()
End Sub
Now execute the add-migration to generate a new migration you will see you new custom name defined in the migration script.
You may be required to similarly override the Generate(DropForeignKeyOperation...) methods as well if you need to downgrade from this configuration _or_ if alter table commands require the key to be dropped first.
I recently came across the wonderful Linqkit library and I want to make use of Generic Predicates to create a function for mapping users to the data they have access too accross any table that contains our data mapping fields. (H1L1, H1L2, etc)
Based on the tutorial (C# only) I see that this is indeed possible but I'm stuck.
So far I've created an interface:
Public Interface IDataMap
ReadOnly Property H1L1() As String
ReadOnly Property H1L2() As String
ReadOnly Property H1L3() As String
ReadOnly Property H2L1() As String
ReadOnly Property H2L2() As String
ReadOnly Property H2L3() As String
ReadOnly Property H3L1() As String
ReadOnly Property H3L2() As String
ReadOnly Property H3L3() As String
End Interface
Adjusted the Linq class for a table I'd like to operate on by adding
Implements IDataMap
and mapped each of the respective classes properties to the interface. I probably should have extended the linq class but for now i've just hardcoded the changes into the class generated by VS.
<Global.System.Data.Linq.Mapping.ColumnAttribute(Storage:="_H1L1", DbType:="VarChar(30)")> _
Public ReadOnly Property H1L1() As String Implements IDataMap.H1L1
Get
Return Me._H1L1
End Get
End Property
But I'm not sure where to go from here... or where to put this function so it's accessible from anywhere in my project. My test function is basic:
Public Shared Function mapUserToData(Of TEntity As IDataMap)(H1L1 As String) As Expression(Of Func(Of TEntity, Boolean))
Return Function(x) (H1L1 = x.H1L1))
End Function
Evenually I want to be able to say something similar to this:
DB.someTables.Where(someTable.mapUserToData("345BDS"))
The only way intellisense allows me to see that "mapUserToData" is available is if I put the function inside of my Linq Class... but then it's not generic. If I put the function inline in my code behind intellisense doesn't see my "mapUserToData" function as a method on my table. Maybe this is because of language/namespace differences between C# and VB.NET?
I'm a newbie to both .Net and Linq so please forgive me in advance for that.
I can use the linqkit predicate function successfully on an adhoc basis using
Dim predicate = PredicateBuilder.False(Of someTable)()
predicate = predicate.Or(Function(p) p.H1L1 IsNot Nothing)
Dim PgmED = (From x In DB.someTables.Where(predicate) Select x).AsEnumerable()
But can't afford to replicate the data mapping logic each time I need it. If anyone knows how to help I will be forever in their debt!
Try putting the mapUserToData function in a module as an Extension Method. Make it an extension of the IDataMap Interface.
<Extension()> _
Public Function mapUserToData(Of TEntity As IDataMap)(ByVal objTarget As IDataMap, H1L1 As String) As Expression(Of Func(Of TEntity, Boolean))
Return Function(x) (H1L1 = x.H1L1)
End Function
suppose I declare a class like this:
Class tst
Public Props As New Dictionary(Of String, MyProp)
End Class
and added properties something along these lines:
Dim t As New tst
t.Props.Add("Source", new MyProp(3))
but now want to access it like this:
t.Source
how can I create a getter without knowing the name of the getter?
Ok, if you insist on "auto-vivifying", the only way I know of to do something like that is to generate the code as a string, and then compile it at runtime using the classes in the System.CodeDom.Compiler namespace. I've only ever used it to generate complete classes from scratch, so I don't know if you could even get it to work for what need to add properties to an already existing class, but perhaps you could if you compiled extension methods at runtime.
The .NET framework includes multiple implementations of the CodeDomeProvider class, one for each language. You will most likely be interested in the Microsoft.VisualBasic.VBCodeProvider class.
First, you'll need to create a CompilerParameters object. You'll want to fill its ReferencedAssemblies collection property with a list of all the libraries your generated code will need to reference. Set the GenerateExecutable property to False. Set GenerateInMemory to True.
Next, you'll need to create a string with the source code you want to compile. Then, call CompileAssemblyFromSource, passing it the CompilerParameters object and the string of source code.
The CompileAssemblyFromSource method will return a CompilerResults object. The Errors collection contains a list of compile errors, if there are any, and the CompiledAssembly property will be a reference to your compiled library (as an Assembly object). To create an instance of your dynamically compiled class, call the CompiledAssembly.CreateInstance method.
If you're just generating a small amount of code, it's pretty quick to compile it. But if it's a lot of code, you may notice an impact on performance.
Here's a simple example of how to generate a dynamic class containing a single dynamic property:
Option Strict Off
Imports System.CodeDom.Compiler
Imports Microsoft.VisualBasic
Imports System.Text
Public Class Form3
Private Sub Button1_Click(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles Button1.Click
Dim code As StringBuilder = New StringBuilder()
code.AppendLine("Namespace MyDynamicNamespace")
code.AppendLine(" Public Class MyDynamicClass")
code.AppendLine(" Public ReadOnly Property WelcomeMessage() As String")
code.AppendLine(" Get")
code.AppendLine(" Return ""Hello World""")
code.AppendLine(" End Get")
code.AppendLine(" End Property")
code.AppendLine(" End Class")
code.AppendLine("End Namespace")
Dim myDynamicObject As Object = generateObject(code.ToString(), "MyDynamicNamespace.MyDynamicClass")
MessageBox.Show(myDynamicObject.WelcomeMessage)
End Sub
Private Function generateObject(ByVal code As String, ByVal typeName As String) As Object
Dim parameters As CompilerParameters = New CompilerParameters()
parameters.ReferencedAssemblies.Add("System.dll")
parameters.GenerateInMemory = True
parameters.GenerateExecutable = False
Dim provider As VBCodeProvider = New VBCodeProvider()
Dim results As CompilerResults = provider.CompileAssemblyFromSource(parameters, code)
If results.Errors.HasErrors Then
Throw New Exception("Failed to compile dynamic class")
End If
Return results.CompiledAssembly.CreateInstance(typeName)
End Function
End Class
Note, I never use Option Strict Off, but for the sake of simplicity in this example, I turned it off so I could simply call myDynamicObject.WelcomeMessage without writing all the reflection code myself.
Calling methods on objects using reflection can be painful and dangerous. Therefore, it can be helpful to provide a base class or interface in a shared assembly which is referenced by both the generated assembly, and the fixed assembly which calls the generated assembly. That way, you can use the dynamically generated objects through a strongly typed interface.
I figured based on your question that you were just more used to dynamic languages like JavaScript, so you were just thinking of a solution using the wrong mindset, not that you really needed to or even should be doing it this way. But, it is definitely useful in some situations to know how to do this in .NET. It's definitely not something you want to be doing on a regular basis, but, if you need to support custom scripts to perform complex validation or data transformations, something like this can be very useful.
Ok, I'm completely at a loss here. I've used DataAnnotations attribute DisplayName successfully using MVC model binding, and even with WPF/Silverlight model binding and of course it works just fine, but now I'm on a project that I'm forced to use VB.NET 3.5 WinForms.
I have a Linq2Sql model and I created a partial class for one of my classes and included a MetadataType attribute to point to a metadata class. I added a DisplayName attribute to a property in the metadata class. I then bind my datagridview with an IQueryable(Of mydatatype), but the column name in the grid is the Property's name and not the DisplayName.
Am I missing something? Is there something else I need to do to get the datagridview to use the DisplayName?
In my Model class:
Imports System.ComponentModel
Imports System.ComponentModel.DataAnnotations
<MetadataType(GetType(vwREVIEW_ECRMetadata))> _
Partial Class vwREVIEW_ECR
Public Sub TestMethod()
End Sub
End Class
Public Class vwREVIEW_ECRMetadata
Private _ECRNumber As String
<DisplayName("ECR #")> _
Public Property ECRNumber() As String
Get
Return _ECRNumber
End Get
Set(ByVal value As String)
_ECRNumber = value
End Set
End Property
End Class
In my Repository class:
Public Function GetAllECRsForLookup() As IQueryable(Of vwREVIEW_ECR)
Return db.vwREVIEW_ECRs
End Function
In my Presenter class:
Public Sub GetData()
view.FillData(model.GetFilteredECRsForLookup())
End Sub
In my View:
Public Sub FillData(ByVal data As System.Linq.IQueryable(Of vwREVIEW_ECR)) Implements ILookupECRView.FillData
Me.uxECRData.DataSource = data
End Sub
Any help would be greatly appreciated! Thanks
Ok, so I found a solution to my problem. Didn't even think about it this way, but in ASP.NET & WPF, you get this behavior becuase of model binding behavior built in. WinForms has databinding as well, but isn't just there for your. Though I could've "bound" my datagridview to my linq2sql generated object in the runtime, which would've accomplished what I needed, I needed to do this at design time, so instead, I modified my MVP to use ViewModels where needed, and bind the datagrid to that object at runtime to get my column names to look the way I want. The ViewModel is hooked up to the model and can pass the real values to it.
I based this approach on this blog, though I didn't fully implement what he did:
http://aviadezra.blogspot.com/2009/08/mvp-mvvm-winforms-data-binding.html
I am trying to make my own Labeller plugin for Cruise Control .Net 1.4.3. I have made a class based on another plug in example but I keep getting an error
Class 'AssemblyVersionLabeller' must implement 'Function Generate(integrationResult As IIntegrationResult) As String' for interface 'ThoughtWorks.CruiseControl.Core.ILabeller'
Here is my code :
Imports Exortech.NetReflector
Imports ThoughtWorks.CruiseControl.Core
Imports ThoughtWorks.CruiseControl.Core.Util
Namespace NetAssembly.CCNet.Label
_
Public Class AssemblyVersionLabeller
Implements ILabeller
Public Sub Run(ByVal result As IIntegrationResult)
result.Label = Generate(result)
End Sub
Public Function Generate(ByVal integrationResult As IIntegrationResult) As String
Dim label As String = integrationResult.LastIntegration.Label
Return label
End Function
<ReflectorProperty("prefix", Required:=False)> _
Public Prefix As String = String.Empty
End Class
End Namespace
What am I doing wrong? What have I missed?
Background Info:
I am using VS2005. I cant use CrusieControl 1.4.4 RC2 (which has an Assembly Labeller) because my source control's plugin (SCM Anywhere) doesnt work with it.
I cannot judge just by looking at your code, but if you need a sample on how to write labellers (C# code though), you could take a look at BrekiLabeller code (written by me).
I believe you forgot the overrides decleration..
Public Overrides Function Generate