What I'm trying to do is create a site in Orchard that doesn't have a way for a user to register. An administrator will create the users.
What I have is module that defines the parts, records, views, etc. That is basically working.
Now what I'm trying to do is add a UserPart (from Orchard.Users) to one of the parts in my module.
I'm not sure how to do that. I need the fields displayed for the UserPart with the fields for the parent part in the same view. This also needs to be done in a way that when a save happens, all of the UserPart fields get sent to the Orchard.Users module.
Any suggestions, pointers or links on how to do that?
Thanks!
UPDATE...
The Activating Filter is an interesting idea. I initially chose the migration route. For now, I'll try and get that method working.
For simplicity, let's say I have a "Company" type (there's more to the actual type) that has a "CompanyName" and a UserPart.
Here's what the different pieces look like...
Migrations.cs (simplified)
public int Create()
{
SchemaBuilder.CreateTable("CompanyPartRecord", table => table.ContentPartRecord()
.Column("CompanyName", DbType.AnsiString, c => c.WithLength(50))
.Column("UserId", DbType.Int32));
SchemaBuilder.CreateForeignKey("FK_CompanyPartRecord_UserPartRecord", "CompanyPartRecord", new[] {"UserId" }, "Orchard.Users", "UserPartRecord", new[] { "Id" })
ContentDefinitionManager.AlterTypeDefinition("Company", type => type.WithPart("CommonPart").WithPart("UserPart"));
}
CompanyPartRecord
public class CompanyPartRecord : ContentPartRecord
{
public virtual string CompanyName { get; set; }
public virtual int? UserId { get; set; }
}
CompanyPart
public class CompanyPart : ContentPart<CompanyPartRecord>
{
internal LazyField<UserPart> UserPartField = new LazyField<UserPart>();
public string CompanyName
{
get { return Record.CompanyName; }
set { Record.CompanyName = value; }
}
public UserPart User
{
get { return UserPartField.Value;}
set { UserPartField.Value = value; }
}
}
Handler
public class CompanyPartHandler : ContentHandler
{
private readonly IContentManager _manager;
public CompanyPartHandler(IRepository<CompanyPartRecord> repository, IContentManager manager)
{
_manager = manager;
Filters.Add(StorageFilter.For(repository));
OnActivated<CompanyPart>(OnActivatedHandler);
}
private void OnActivatedHandler(ActivatedContentContext context, CompanyPart part)
{
if(part.User == null)
{
part.User = _manager.Create<UserPart>("User");
}
else
{
part.User = _manager.Get<UserPart>(part.User.Id);
}
}
}
Driver
public class CompanyPartDriver : ContentPartDriver<CompanyPart>
{
protected override DriverResult Editor(CompanyPart part, dynamic shapeHelper)
{
return ContentShape("Parts_Company_Edit", () => shapeHelper.EditorTemplate(TemplateName: "Parts/Company",
Model: part, Prefix: Prefix));
}
protected override DriverResult Editor(CompanyPart part, IUpdateModel updater, dynamic shapeHelper)
{
updater.TryUpdateModel(part, Prefix, null, null);
return Editor(part, shapeHelper);
}
}
Controller
public class AdminCompanyController : Controller, IUpdateModel
{
private readonly IOrchardServices _services;
private readonly INotifier _notifier;
private readonly IContentManager _contentManager;
private readonly ITransactionManager _transactionManager;
private readonly Localizer T = NullLocalizer.Instance;
public AdminCompanyController(IOrchardServices services)
{
_services = services;
_notifier = services.Notifier;
_contentManager = services.ContentManager;
_transactionManager = services.TransactionManager;
}
public ActionResult Create()
{
var company = _contentManager.New<CompanyPart>("Company");
var model = _contentManager.BuildEditor(company);
return View(model);
}
[HttpPost, ActionName("Create")]
public ActionResult CreatePOST()
{
var contentItem = _contentManager.New<CompanyPart>("Company");
var model = _contentManager.UpdateEditor(contentItem, this);
if (!ModelState.IsValid)
{
_transactionManager.Cancel();
return View(model);
}
_contentManager.Create(contentItem.ContentItem);
_notifier.Information(T("Company has been saved"));
return RedirectToAction("Index");
}
public ActionResult Edit(int Id)
{
var contentItem = _services.ContentManager.Get(Id);
dynamic model = _services.ContentManager.BuildEditor(contentItem);
return View(model);
}
[HttpPost, ActionName("Edit")]
public ActionResult EditPOST(int Id)
{
var contentItem = _contentManager.Get<CompanyPart>(Id);
var model = _contentManager.UpdateEditor(contentItem, this);
_notifier.Information(T("Company has been saved"));
return RedirectToAction("Index");
}
public ActionResult Delete(int Id)
{
var contentItem = _contentManager.Get<CompanyPart>(Id);
_contentManager.Destroy(contentItem.ContentItem);
return RedirectToAction("Index");
}
bool IUpdateModel.TryUpdateModel<TModel>(TModel model, string prefix, string[] includeProperties, string[] excludeProperties)
{
return TryUpdateModel(model, prefix, includeProperties, excludeProperties);
}
public void AddModelError(string key, LocalizedString errorMessage)
{
ModelState.AddModelError(key, errorMessage.ToString());
}
}
View (create)
#{ Layout.Title = T("Add Company").ToString(); }
#using (Html.BeginFormAntiForgeryPost())
{
#Display(Model)
}
Editor Template
#model SDS.Models.CompanyPart
<fieldset>
#Html.LabelFor(m => m.CompanyName)
#Html.TextBoxFor(m => m.CompanyName)
</fieldset>
#*
What goes here to display UserPart?
*#
So here's where I'm at. I can see the ContentItem (CompanyType). I can put in the name and save it. The name is getting saved to the db. Right now the UserPart is getting saved to the db, but all of the fields are blank.
The part I'm stuck on is what to put in the editor template to display the UserPart fields so that the values get to the UserPart driver and ultimately the db.
Any ideas on how to do that?
Thanks!
So you don't attach parts to parts, you attach parts to content items, and you can do that in multiple ways.
You can do it through the admin screen, but that isn't a code driven solution and would have problems if you have multiple environments or need to redeploy a fresh version of code.
You can attach the part when you create a new content item in the migration. This might be a good solution, if you already ran your migration you could possibly do it with an update migration. This allows the part to be managed through the admin screen, but has downsides because it can be removed and if you have code that relies on the part then you will start having errors.
The last way and best way is to attach the part dynamically using an Activating Filter.
ActivatingFilter class - Attaches a part to a content type from code. As opposed to attaching parts via migrations, parts attached using this filter will neither be displayed in the Dashboard, nor users will be able to remove them from types. It's a legitimate way of attaching parts that should always exist on a given content type.
So to do this:
1. Add a reference to Orchard.Users to your custom project.
2. Create a handler for you part. Such as MyPartHandler
3. Then add the activating handler like so
Filters.Add(ActivatingFilter.For<UserPart>("MyContentType"));
So now anywhere in your code you can access the UserPart if you already have your part, or the content item using
var userPart = myPart.As<UserPart>();
Related
I'm developing an Azure Mobile App service to interface to my Xamarin application.
I've created, connected and successfully populated an SQL Database, but when I try to add some filters to my request, for example an orderby() or where() clauses, it returns me a Bad Request error.
For example, this request: https://myapp.azurewebsites.net/tables/Race?$orderby=iRound%20desc,iYear%20desc&$top=1&ZUMO-API-VERSION=2.0.0 gives me {"message":"The query specified in the URI is not valid. Could not find a property named 'IYear' on type 'MyType'."}.
My configuration method is this:
HttpConfiguration config = new HttpConfiguration();
new MobileAppConfiguration()
.AddTablesWithEntityFramework()
.ApplyTo(config);
config.MapHttpAttributeRoutes();
Database.SetInitializer(new CreateDatabaseIfNotExists<MainDataContext>());
app.UseWebApi(config);
and my DbContext is this:
public class MainDataContext : DbContext
{
private const string connectionStringName = "Name=MS_TableConnectionString";
public MainDataContext() : base(connectionStringName)
{
Database.Log = s => WriteLog(s);
}
public void WriteLog(string msg)
{
System.Diagnostics.Debug.WriteLine(msg);
}
protected override void OnModelCreating(DbModelBuilder modelBuilder)
{
modelBuilder.Conventions.Add(
new AttributeToColumnAnnotationConvention<TableColumnAttribute, string>(
"ServiceTableColumn", (property, attributes) => attributes.Single().ColumnType.ToString()));
}
public DbSet<Race> Race { get; set; }
public DbSet ...ecc...
}
Following this guide, I added a migration after creating my TableControllers. So the TableController for the example type shown above is pretty standard:
[EnableQuery(AllowedQueryOptions = AllowedQueryOptions.All)]
public class RaceController : TableController<Race>
{
protected override void Initialize(HttpControllerContext controllerContext)
{
base.Initialize(controllerContext);
MainDataContext context = new MainDataContext();
DomainManager = new EntityDomainManager<Race>(context, Request);
}
// GET tables/Race
[EnableQuery(AllowedQueryOptions = AllowedQueryOptions.All)]
public IQueryable<Race> GetAllRace()
{
return Query();
}
// GET tables/Race/48D68C86-6EA6-4C25-AA33-223FC9A27959
public SingleResult<Race> GetRace(string id)
{
return Lookup(id);
}
// PATCH tables/Race/48D68C86-6EA6-4C25-AA33-223FC9A27959
public Task<Race> PatchRace(string id, Delta<Race> patch)
{
return UpdateAsync(id, patch);
}
// POST tables/Race
public async Task<IHttpActionResult> PostRace(Race item)
{
Race current = await InsertAsync(item);
return CreatedAtRoute("Tables", new { id = current.Id }, current);
}
// DELETE tables/Race/48D68C86-6EA6-4C25-AA33-223FC9A27959
public Task DeleteRace(string id)
{
return DeleteAsync(id);
}
}
As you can see, I already tried to add the EnableQuery attribute to my TableController, as seen on Google. I also tried to add these filters to the HttpConfiguration object, without any success:
config.Filters.Add(new EnableQueryAttribute
{
PageSize = 10,
AllowedArithmeticOperators = AllowedArithmeticOperators.All,
AllowedFunctions = AllowedFunctions.All,
AllowedLogicalOperators = AllowedLogicalOperators.All,
AllowedQueryOptions = AllowedQueryOptions.All
});
config.AddODataQueryFilter(new EnableQueryAttribute
{
PageSize = 10,
AllowedArithmeticOperators = AllowedArithmeticOperators.All,
AllowedFunctions = AllowedFunctions.All,
AllowedLogicalOperators = AllowedLogicalOperators.All,
AllowedQueryOptions = AllowedQueryOptions.All
});
I don't know what to investigate more, as things seems to be changing too fast for a newbie like me who's first got into Azure.
EDIT
I forgot to say that asking for the complete table, so for example https://myapp.azurewebsites.net/tables/Race?ZUMO-API-VERSION=2.0.0, returns correctly the entire dataset. The problem occurs only when adding some clauses to the request.
EDIT 2
My model is like this:
public class Race : EntityData
{
public int iRaceId { get; set; }
public int iYear { get; set; }
public int iRound { get; set; }
ecc..
}
and the database table that was automatically created is this, including all the properties inherited from EntityData:
Database table schema
Digging into the source code, Azure Mobile Apps sets up camelCase encoding of all requests and responses. It then puts them back after transmission accordign to rules - so iRaceId becomes IRaceId on the server.
The easiest solution to this is to bypass the auto-naming and use a JsonProperty attribute on each property within your server-side DTO and client-side DTO so that they match and will get encoding/decoded according to your rules.
So:
public class Race : EntityData
{
[JsonProperty("id")]
public string Id { get; set; }
[JsonProperty("raceId")]
public int iRaceId { get; set; }
[JsonProperty("year")]
public int iYear { get; set; }
[JsonProperty("round")]
public int iRound { get; set; }
etc..
}
Okay so I am using Entity Framework 6.1 and attempting code first with seeding. I have a drop always intializer that ALWAYS WORKS. However I want to use database migration which I have set up and it works. UNTIL I normalize out a table and then try to seed it I get a primary key error.
Basically in my context when I uncomment out the changes in the 'TODO' section and the schema changes I get a primary key violation when attempting population of the newly normalized out table. It will work for the initializer that does the drop always, but I want my migration data table and not to drop the database everytime I make changes in case I want to rollback ever. I have tried changing the attribute of the 'PersonId' to Identity and to None and back to Identity. So the caveat is if it is set to 'Identity' it will work but the values will keep incrementing to higher values each time 1,2,3,4 then 5,6,7,8;etc. If I set it to none it works the first time and then when it is split in the mapping and normalized it blows up. I have tried custom dbcc commands and it does not like that either, as even with setting dbcc to reseed with the two new tables it does not like it. It is as if it has no idea about seeding the new table when being done explicitly.
Does anyone know how to do a seeding process that the model can handle if you normalize out the mapping of an object to multiple tables? I am trying a bunch of different patterns and getting nowhere fast.
So POCO Object
public class Person
{
[DatabaseGenerated(DatabaseGeneratedOption.None)]
public int PersonId { get; set; }
[Column(TypeName = "varchar")]
[Required]
[MaxLength(32)]
public string FirstName { get; set; }
[Column(TypeName = "varchar")]
[Required]
[MaxLength(32)]
public string LastName { get; set; }
[Column(TypeName = "varchar")]
public string OverlyLongDescriptionField { get; set; }
}
Context for code First:
public class EasyContext : DbContext
{
public EasyContext() : base("name=EasyEntity")
{
//Database.SetInitializer<EasyContext>(new EasyInitializer());
Database.SetInitializer(new MigrateDatabaseToLatestVersion<EasyContext, Migrations.Configuration>("EasyEntity"));
}
public DbSet<ProductOrder> ProductOrder { get; set; }
public DbSet<Person> Person { get; set; }
public DbSet<Product> Product { get; set; }
public DbSet<Audit> Backup { get; set; }
protected override void OnModelCreating(DbModelBuilder modelBuilder)
{
modelBuilder.HasDefaultSchema("dbo");
modelBuilder.Conventions.Remove<PluralizingTableNameConvention>();
modelBuilder.Conventions.Remove<ManyToManyCascadeDeleteConvention>();
//TODO Let's normalize out a long descriptive field
//modelBuilder.Entity<Person>()
//.Map(m =>
//{
// m.Properties(p => new { p.FirstName, p.LastName });
// m.ToTable("Person");
//})
//.Map(m =>
//{
// m.Properties(p => new { p.OverlyLongDescriptionField });
// m.ToTable("PersonDescription");
//});
}
}
Initializer for DropCreateAlways:
public class EasyInitializer : DropCreateDatabaseAlways<EasyContext>
{
protected override void Seed(EasyContext context)
{
SeedingValues.SeedingForDatabaseDrop(context);
base.Seed(context);
}
}
Configuration for migrations:
internal sealed class Configuration : DbMigrationsConfiguration<EasyEntity.EasyContext>
{
public Configuration()
{
AutomaticMigrationsEnabled = true;
AutomaticMigrationDataLossAllowed = true;
ContextKey = "EasyEntity.EasyContext";
}
protected override void Seed(EasyContext context)
{
SeedingValues.SeedingWithoutDatabaseDrop(context);
base.Seed(context);
}
}
Base Seeding class:
internal static class SeedingValues
{
public static void SeedingForDatabaseDrop(EasyContext context)
{
BaseSeed(context);
}
public static void SeedingWithoutDatabaseDrop(EasyContext context)
{
context.Person.ClearRange();
BaseSeed(context);
}
private static void BaseSeed(EasyContext context)
{
IList<Person> persons = new List<Person>
{
new Person { PersonId = 1, FirstName = "Brett", LastName = "Guy", OverlyLongDescriptionField = "OMG Look I have a bunch of text denormalizing a table by putting a bunch of stuff only side related to the primary table." },
new Person { PersonId = 2, FirstName = "Neil", LastName = "Person"},
new Person { PersonId = 3, FirstName = "Ryan", LastName = "Other"},
new Person { PersonId = 4, FirstName = "Aaron", LastName = "Dude"},
};
foreach (var person in persons)
context.Person.AddOrUpdate(person);
}
}
ClearingHelper
public static void ClearRange<T>(this DbSet<T> dbSet) where T : class
{
using (var context = new EasyContext())
{
dbSet.RemoveRange(dbSet);
}
}
Okay so the issue is with a newly created table being not populated and the old table being populated. So if I have following off of my example a POCO class 'Person' and have plurilization removed. Without any explicit mapping and just a DbSet will create a table Person. If I then do my mapping to split tables.
modelBuilder.Entity<Person>()
.Map(m =>
{
m.Properties(p => new { p.PersonId, p.FirstName, p.LastName });
m.ToTable("Person");
})
.Map(m =>
{
m.Properties(p => new { p.PersonId, p.OverlyLongDescriptionField });
m.ToTable("PersonDescription");
});
I get a Primary Key violation with my seeding process. This is due to if I look at the database newly updated it still is retaining the old table and created a new one. However it does not know how to remove the data with this method:
public static void ClearRange<T>(this DbSet<T> dbSet) where T : class
{
using (var context = new EasyContext())
{
dbSet.RemoveRange(dbSet);
}
}
Becase the set is partial. So I am thinking: "Well if my data for seeding is contained at this point and I need to alter it moving forward I can in theory just clean up the tables directly with SQL commands." This is not the approach I wanted per say but it does work.
So I add more data to my clearing helper:
public static void ResetIdentity(string tableName)
{
using (var context = new EasyContext())
{
context.Database.ExecuteSqlCommand($"DBCC CHECKIDENT('{tableName}', RESEED, 0)");
}
}
public static void DeleteTable(string tableName)
{
using (var context = new EasyContext())
{
context.Database.ExecuteSqlCommand($"DELETE {tableName}");
}
}
public static void DeleteTableAndResetIdentity(string tableName)
{
using (var context = new EasyContext())
{
context.Database.ExecuteSqlCommand($"DELETE {tableName}");
context.Database.ExecuteSqlCommand($"DBCC CHECKIDENT('{tableName}', RESEED, 0)");
}
}
Then I add this to my Seeding routine's cleanup portion:
ClearingHelper.DeleteTable("dbo.PersonDescription");
ClearingHelper.DeleteTableAndResetIdentity("dbo.Person");
This is unfortunate in two ways to do it this way in that:
It is slower doing a delete as it goes row by row.
If I migrate backwards I will have to change this.
But it works! I can now have changes to the schema with normalizing out POCO's and still run a seeding routine.
I'm trying to strongly type (such as it is) some URLs for a web app when I build a viewmodel.
So I have something like:
new MyModel {
Text = "Foo",
Url = new UrlHelper(Request.RequestContext).Action("MyAction")
}
This works just fine in a controller method, but I have another situation where I am not receiving the Request.Context because it's being called in another class.
Is there another way to do this so that I'm not using "magic strings" and/or relying on the context object?
Use Reference
HttpContext.Current
which is derived from system.web. There for following code will work anywhere in your application.
UrlHelper objUrlHelper = new UrlHelper(HttpContext.Current.Request.RequestContext);
objUrlHelper.Action("About");
Example:
public class MyViewModel
{
public int ID { get; private set; }
public string Link
{
get
{
UrlHelper objUrlHelper = new UrlHelper(HttpContext.Current.Request.RequestContext);
return objUrlHelper.Action("YourAction", "YourController", new { id = this.ID });
}
}
public MyViewModel(int id)
{
this.ID = id;
}
}
I have created a custom content type called 'AccessFolder'. I can see it in the list of content types and can create a new one. When I create a new AccessFolder, I get my editor template that I created for it. After I enter the information and click save, I'm directed to a Not Found page however the indicator message tells me my AccessFolder was created successfully.
In the driver, I can see the model after it is bound using the updater.TryUpdateModel. The correct values are assigned to the model's properties.
It just never gets to the database.
AccessFolderPart:
public class AccessFolderPart : ContentPart<AccessFolderPartRecord>
{
public virtual string Name
{
get { return Record.Name; }
set { Record.Name = value; }
}
public virtual IEnumerable<RoleRecord> DownloadRoles
{
get { return Record.DownloadRoles.Select(x => x.RoleRecord); }
}
}
AccessFolderPartRecord
public class AccessFolderPartRecord : ContentPartRecord
{
public virtual string Name { get; set; }
public virtual List<ContentAccessFolderRoleRecord> DownloadRoles { get; set; }
}
Relevant Pieces of AccessFolderPartDriver
protected override DriverResult Editor(AccessFolderPart part, dynamic shapeHelper)
{
var viewModel = new AccessFolderViewModel(part, _roleService.GetRoles());
return ContentShape("Parts_AccessFolder_Edit", () =>
shapeHelper.EditorTemplate(TemplateName: templateName, Model: viewModel, Prefix: Prefix));
}
protected override DriverResult Editor(AccessFolderPart part, Orchard.ContentManagement.IUpdateModel updater, dynamic shapeHelper)
{
var viewModel = new AccessFolderViewModel { Part = part };
updater.TryUpdateModel(viewModel, Prefix, null, null);
if (part.ContentItem.Id != 0)
{
_roleService.UpdateRolesForAccessFolder(part.ContentItem, part.DownloadRoles);
}
return Editor(part, shapeHelper);
}
I've been stuck on this since Friday. I've created custom types before and never had any problems with it. I can't see what I've done wrong here.
Update - Added content Handler class
Here's the one line for the handler:
public class AccessFolderPartHandler : ContentHandler
{
public AccessFolderPartHandler(IRepository<AccessFolderPartRecord> repository)
{
Filters.Add(StorageFilter.For(repository));
}
}
I Think you are missing the proper mapping on your driver:
if (updater.TryUpdateModel(viewModel, Prefix, null, null))
{
part.Name= viewModel.Name;
if (part.ContentItem.Id != 0)
{
_roleService.UpdateRolesForAccessFolder(part.ContentItem, part.DownloadRoles);
}
}
This is an editor for WebShop Global Settings. I needed to extend the editor with a ViewModel. It worked fine before I started but now crashes with the above error when it's invoked. What am I doing wrong?
Here's the driver:
public class WebShopSettingsPartDriver : ContentPartDriver<WebShopSettingsPart>
{
private readonly ISiteService _siteService;
private readonly IWebshopSettingsService _webshopSettings;
protected override string Prefix { get { return "WebShopSettings"; } }
private const string shapeName = "Parts_WebShopSettings_Edit";
private const string templateName = "Parts/WebShopSettings";
public WebShopSettingsPartDriver(IWebshopSettingsService webshopSettings, ISiteService siteService)
{
_webshopSettings = webshopSettings;
_siteService = siteService;
}
protected override DriverResult Editor(WebShopSettingsPart part, dynamic shapeHelper)
{
var settings = _siteService.GetSiteSettings().As<WebShopSettingsPart>();
var model = new WebShopSettingsVM
{
WebShopSettings = settings,
ShippingProducts = _webshopSettings.ShippingProductRecords()
};
return ContentShape(shapeName,
() => shapeHelper.EditorTemplate(TemplateName: templateName, Model: model, Prefix: Prefix)).OnGroup("WebShop");
}
}
}
Here is the Handler:
public class WebShopSettingsPartHandler : ContentHandler {
public WebShopSettingsPartHandler(IRepository<WebShopSettingsRecord> repository) {
T = NullLocalizer.Instance;
Filters.Add(new ActivatingFilter<WebShopSettingsPart>("Site"));
Filters.Add(StorageFilter.For(repository));
OnGetContentItemMetadata<WebShopSettingsPart>((context, part) => context.Metadata.EditorGroupInfo.Add(new GroupInfo("WebShop")));
}
}
And here is the first line of the View (which is in Views\EditorTemplates\Parts\WebShopSettings.cshtml):
#model Cascade.WebShop.ViewModels.WebShopSettingsVM
The Placement.ini file has the following entry:
<Place Parts_WebShopSettings_Edit="Content:0" />
Here is the ViewModel:
public class WebShopSettingsVM
{
public IEnumerable<ShippingProductRecord> ShippingProducts{ get; set; }
[Required]
public int? ShippingProductRecordId { get; set; }
public WebShopSettingsPart WebShopSettings { get; set; }
// Expose all the properties of the Part directly on the VM
[Required]
public string AdministratorEmailAddress
{
get { return WebShopSettings.AdministratorEmailAddress; }
set { WebShopSettings.AdministratorEmailAddress = value; }
}
[Required]
public string ContinueShoppingUrl
{
get { return WebShopSettings.ContinueShoppingUrl; }
set { WebShopSettings.ContinueShoppingUrl = value; }
}
// and so on...
}
After Bertrand's suggestion below I updated the View to:
#using Cascade.WebShop.ViewModels
#using Cascade.WebShop.Models
#{
var vm = Model.Model as WebShopSettingsVM;
}
<fieldset>
<legend>#T("Webshop")</legend>
<div>
<label for="#Html.FieldIdFor(x=>vm.AdministratorEmailAddress)">#T("Administrator email address")</label>
#Html.TextBoxFor(x=>vm.AdministratorEmailAddress, new { #class = "textMedium" })
#Html.ValidationMessage("AdministratorEmailAddress", "*")
...
Insights and suggestions greatly appreciated -- I simply can't see what's wrong.
A second copy of the driver, under a slightly different name, was present in the 'Helpers' directory. Not surprisingly, I didn't notice this. The 'Helper' driver was supplying a Part and the 'proper' driver a VM. Both were being fired and thus whether I used a VM or a Part one or other of the two drivers would throw an exception.
Deleting the spurious driver fixed the problem. Sorry Bertrand.
The model is still the shape. Remove the directive, and access Model.Model to access your view model: Model is the shape, and Model.Model is the property named Model that is on the shape.
Just cast Model.Model to the view model's type.