Calling multiple Blazor components with two binding - components

I have a blazor component:
#inject IJSRuntime JSRuntime
#using bullDocDBAcess
#using bullDocDBAcess.Models
#inject ITagData _db
#if(tags == null){
<p><em>Loading...</em></p>
}else{
<input type="text" #bind=#tags data-role="tagsinput" />
<input type="hidden" #onchange="onChange" value=#tags />
}
#code{
private string tags = "";
private List<TagModel> tagsList;
private Task<IJSObjectReference> _module;
private Task<IJSObjectReference> Module => _module ??= JSRuntime.InvokeAsync<IJSObjectReference>("import", "./components/tagsinput/tags_inputs_imports.js").AsTask();
[Parameter]
public int organizationId { get; set; } = 0;
[Parameter]
public int documentId { get; set; } = 0;
[Parameter] public EventCallback<Notification> OnTagsChange { get; set; }
private void onChange(Microsoft.AspNetCore.Components.ChangeEventArgs args)
{
tags = (string) args.Value;
OnTagsChange.InvokeAsync(new Notification(tags, documentId.ToString()));
}
protected override async Task OnInitializedAsync()
{
if(organizationId == 0){ //inserção
tags = "";
}else{
tagsList = await _db.GetTagsFromDocument(organizationId, documentId);
foreach (TagModel t in tagsList){
if(String.IsNullOrEmpty(tags)){
tags += t.nome;
}else{
tags += "," + t.nome;
}
}
}
var module = await Module;
await module.InvokeVoidAsync("loadScripts");
}
public async ValueTask DisposeAsync()
{
if (_module != null)
{
var module = await _module;
await module.DisposeAsync();
}
}
}
Basically, the idea is to use a bootstrap input tag (called using jsinterop) and include it several times for each product in a table. When tags and updated in this table, the database should be updates with the tags for that specific values.
The parent component:
(...)
#for(int i=0;i<this.document.Count;i++)
{
DocumentModel p = this.document.ElementAt(i);
<tr>
<td><TagManagement #key=#p.id organizationId=#p.organizacao_id documentId=#p.id OnTagsChange="TagsHandler" /></td>
</tr>
}
(...)
void TagsHandler(Notification notification)
{
Console.WriteLine("Hello"); //only for testing purposes
}
A two-way binding is implemented between the parent and child. However, o have no idea how to handle multiple component invocation with callback function for each one. When a new tag is inserted, only works on the first element of the table and the event is fired two times (because in this test I only have two rows).
I tried to include the key attribute but didn't work. Can you please help me?
Best regards

Related

Passing a list to partialview, BeginCollectionItem()

I want to pass a list to PartialView that has BeginCollectionItem(). Here is the code,
InquiryOrderViewModel
public class InquiryOrderViewModel
{
public InquiryOrder InquiryOrder { get; set; }
public List<InquiryOrderDetail> InquiryOrderDetails { get; set; }
public List<InquiryComponentDetail> InquiryComponentDetails { get; set; }
}
InquiryComponentDetail model
public class InquiryComponentDetail
{
[Key]
public int InquiryComponentDetailId { get; set; }
public int DesignCodeId { get; set; }
public int QualityReferenceId { get; set; }
public int Height { get; set; }
public int Length { get; set; }
public int GscmComp { get; set; }
public int Wastage { get; set; }
public int TotalYarn { get; set; }
public virtual DesignCodeQltyRef DesignCodeQltyRef { get; set; }
}
InquiryOrderIndex View and the Script to render multiple items at once
#model eKnittingData.InquiryOrderViewModel
#using (Html.BeginForm("Save", "InquiryOrder"))
{
..........
<div id="cmpDts">
#foreach (var item in Model.InquiryComponentDetails)
{
}
</div>
..........
}
<script>
var prev;
$(document).on('focus', '.class03', function () {
prev = $(this).val();
}).on('change', '.class03', function () {
if (prev != "") {
$.ajax({
url: '#Url.Action("ComponentDts", "InquiryOrder")', // dont hard code your url's
type: "GET",
data: { DesignCdId: $(this).val() }, // pass the selected value
success: function (data) {
$('.cmpCls').last().replaceWith(data);
}
});
}
else {
$.ajax({
url: '#Url.Action("ComponentDts", "InquiryOrder")', // dont hard code your url's
type: "GET",
data: { DesignCdId: $(this).val() }, // pass the selected value
success: function (data) {
$(".class03 option[value='']").remove();
$('#cmpDts').append(data);
}
});
}
});
</script>
The _DetailEditorRow PartialView which gives ddls with class03 and in main view where it got appended.(This is just to show you what is class03)
#model eKnittingData.InquiryOrderDetail
#using eKnitting.Helpers
#using (Html.BeginCollectionItem("InquiryOrderDetails"))
{
<div class="editorRow">
#Html.DropDownListFor(a => a.ComponentId, (SelectList)ViewBag.CompList, "Select", new { Class = "class02" })
#Html.DropDownListFor(a => a.DesignCodeId, (SelectList)ViewBag.DCodeList, "Select", new { Class = "class03" })
#Html.TextBoxFor(a => a.NoOfParts, new { Class = "class01" })
delete
</div>
}
and in main view it got appended to
<div id="editorRows">
#foreach (var item in Model.InquiryOrderDetails)
{
Html.RenderPartial("_DetailEditorRow", item);
}
</div>
_ComponentDetails PartialView to render items(a list has been passed at once)
#model List<eKnittingData.InquiryComponentDetail>
#using eKnitting.Helpers
<div class="cmpCls">
#foreach(var icd in Model)
{
using (Html.BeginCollectionItem("InquiryComponentDetails"))
{
<div class="innerCmpCls">
#Html.DisplayFor(a => icd.DesignCodeId)
#Html.DisplayFor(a => icd.QualityReferenceId)
#Html.TextBoxFor(a => icd.Height, new { Class="clsHeight clsSameHL"})
#Html.TextBoxFor(a => icd.Length, new { Class = "clsLength clsSameHL" })
#Html.TextBoxFor(a => icd.GscmComp, new { Class = "clsGscmComp clsSameHL" })
#Html.TextBoxFor(A => icd.Wastage, new { Class = "clsWastage" })
#Html.ActionLink("Fds", "View", new { id = icd.QualityReferenceId }, new { #class = "myLink", data_id = icd.QualityReferenceId })
#Html.TextBoxFor(a => icd.TotalYarn, new { Class = "clsTotalYarn" })
<br>
<div class="popFds"></div>
</div>
}
}
</div>
ActionResult that Passes a list at once and returns the PartialView
public ActionResult ComponentDts(int DesignCdId)
{
var objContext = new KnittingdbContext();
var QltyRefList = objContext.DesignCodeQltyRefs.Where(a=>a.DesignCodeId==DesignCdId).ToList();
var iocdList = new List<InquiryComponentDetail>();
foreach(DesignCodeQltyRef dcqr in QltyRefList)
{
iocdList.Add(new InquiryComponentDetail {
DesignCodeId=dcqr.DesignCodeId,
QualityReferenceId=dcqr.QltyRefId
});
}
return PartialView("_ComponentDetails", iocdList);
}
ActionResult for GET
var objContext = new KnittingdbContext();
var newIovm = new InquiryOrderViewModel();
var newIo = new InquiryOrder();
var iocdL = new List<InquiryComponentDetail>();
newIovm.InquiryOrder = newIo;
newIovm.InquiryComponentDetails = iocdL;
return View(newIovm);
ActionResult for POST
public ActionResult Save(InquiryOrderViewModel inquiryOrderViewModel)
{
.........
}
When user selects an item from a dropdownlist(class03), the items related to that item are rendered to the view using the PartialView(_ComponentDetails') and get appended. Then user selects another item from another ddl(class03), the related items are rendered and appended after earlier appended ones. User can go on like this.
Rendering and appending items works fine. But for the PostBack even though i get the number of items in the list correctly(I checked it by putting a break point on POST ActionResult ) all items content show null values. Pls guide me in the correct way for achieving this. All help appreciated. Thanks!
Your _ComponentDetails view is generating form controls that have name attributes that look like (where ### is a Guid)
name="InquiryComponentDetail[###].icd.Height"
which does not match your model because typeof InquiryComponentDetail does not contain a property named icd. In order to bind to your model, your name attribute would need
name="InquiryComponentDetail[###].Height"
To generate the correct html, you will need 2 partials
_ComponentDetailsList.cshtml (this will be called by the ComponentDts() method using return PartialView("_ComponentDetailsList", iocdList);)
#model List<eKnittingData.InquiryComponentDetail>
<div class="cmpCls">
#foreach(var item in Model)
{
Html.RenderPartial("_ComponentDetails", item);
}
</div>
_ComponentDetails.cshtml
#model eKnittingData.InquiryComponentDetail
using (Html.BeginCollectionItem("InquiryComponentDetails"))
{
<div class="innerCmpCls">
#Html.DisplayFor(a => a.DesignCodeId)
#Html.DisplayFor(a => a.QualityReferenceId)
#Html.TextBoxFor(a => a.Height, new { #class="clsHeight clsSameHL"}) // use #class, not Class
#Html.TextBoxFor(a => a.Length, new { Class = "clsLength clsSameHL" })
....
</div>
}

MVC 5 Ajax.BeginForm POST does not bind to model in conroller

Sorry for the amount of code here but it's the best way to explain what;s happening.
I gave this code in my MVC 5 partial view:
#model Hide.MVC.MFC.Models.RequestDataModel
using (Ajax.BeginForm("RequestSetCanceledState", "Home", null,
new AjaxOptions
{
HttpMethod = "POST",
InsertionMode = InsertionMode.Replace,
UpdateTargetId = "request-info",
OnBegin = "AjaxAbsoluteLoaderOn",
OnComplete = "AjaxAbsoluteLoaderOff"
}, new { id = "formCancel" }))
{
<input id="sbmtCancel" type="submit" value="Cancel" />
#Html.DropDownListFor(m => m.CancelReason, new SelectList(Model.CancelReasons, "Id", "Name", Model.CancelReason), String.Empty)
#Html.TextBoxFor(m => m.Reason)
#Html.HiddenFor(m => m.RequestId)
}
My controller action is as follows:
[HttpPost]
public PartialViewResult RequestSetCanceledState(RequestDataModel model)
{
...
return PartialView("....", model);
}
And my model is as follows:
public class RequestDataModel
{
public RequestDataModel() { }
public RequestDataModel(int requestId)
{
this.RequestId = requestId;
using (var service = new InnerServiceClient())
{
var request = service.GetRequest(requestId);
this.State = request.ServiceState;
if (request.PremiseInformationId.HasValue)
{
this.PremisInformation = service.GetExistingData(requestId) ?? new PremiseInformationBL();
}
}
}
public int RequestId { get; set; }
public int State { get; set; }
public PremiseInformationBL PremisInformation { get; set; }
public int? CancelReason { get; set; }
public string Reason { get; set; }
public List<ListItem> CancelReasons
{
get
{
using (var service = new DictionaryServiceClient())
{
return service.GetShortList(TypeDictionary.MFCCancelReason).ToList();
}
}
}
}
The code does post to the RequestSetCanceledState method, and browser send message
CancelReason=1&Reason=123&RequestId=48&X-Requested-With=XMLHttpRequest
but the model is always empty. Also, if I use a GET request, the model is not empty! I tried to remove PremisInformation and CancelReasons from the model, but it did not help. Can someone please tell me why this might be?

Orchard Projection Page Default View

I am using Orchard 1.8 and have created a new content type (called PressRelease), query for the results and projection to view the query with a custom template (using URL Alternates in the format List-ProjectionPage-url-PressRelease.cshtml) and all of that is working fine.
The one part that has me stumped is, if I use The Theme Machine as my theme (untouched), this projection view will show up in an unordered list with the corresponding AutoRoute links to the individual ContentItem entities, their metadata and so on. I'm trying to figure out how I access things such as the AutoRoute URL for a specific item, the metadata (create/publish dates) and so on for use with things like a Facebook Share button. Essentially I'm trying to recreate that default view, albeit with customizations.
Here is the code for List-ProjectionPage-url-PressRelease.cshtml:
#using Orchard.Utility.Extensions;
#using System.Linq
#functions
{
public class PressRelease
{
public PressRelease()
{
this.Attachments = new List<Attachment>();
}
public string Title { get; set; }
public string Source { get; set; }
public DateTime PublishDate { get; set; }
public string Body { get; set; }
public List<Attachment> Attachments { get; set; }
}
public class Attachment
{
public string Filename { get; set; }
public string Path { get; set; }
}
}
#{
//add list of dynamic objects to strongly typed class
var releases = new List<PressRelease>();
foreach (var item in #Model.Items)
{
var release = new PressRelease
{
Title = item.ContentItem.TitlePart.Title,
Source = item.ContentItem.PressRelease.Source.Value,
PublishDate = item.ContentItem.PressRelease.Date.DateTime,
Body = item.ContentItem.BodyPart.Text
};
//load attachment(s) to class
var attachments = (Orchard.MediaLibrary.Fields.MediaLibraryPickerField)item.ContentItem.PressRelease.Attachment;
if (attachments.MediaParts.Count() > 0)
{
foreach (var part in attachments.MediaParts)
{
release.Attachments.Add(new Attachment { Filename = part.FileName, Path = part.MediaUrl });
}
}
releases.Add(release);
}
}
#{
foreach (var item in releases)
{
<div class="press-release">
<div class="press-release-title">#item.Title</div>
<div class="press-release-meta">
<span class="press-release-source">Source: #item.Source</span>
#if (item.PublishDate != DateTime.MinValue)
{
<span class="press-release-date">#item.PublishDate.ToShortDateString()</span>
}
</div>
#if (item.Attachments.Count() > 0)
{
<div class="press-release-attachments">
<span class="press-release-attachments-title">Attached: </span>
#foreach (var attachment in item.Attachments)
{
var linkText = attachment.Filename;
var url = attachment.Path;
#Html.Link(linkText, url);
if (attachment != item.Attachments.Last())
{
<span>, </span>
}
}
</div>
}
<div class="press-release-body">
<p>#Html.Raw(item.Body.Replace("\r\n", "<br />"))</p>
</div>
</div>
<div class="social">
<!-- ** This is where I need AutoRoute URL so I can do FB share link **-->
<div class="fb-share-button" data-href="" data-type="button_count"></div>
</div>
if (item != releases.Last())
{
<hr />
}
}
}
Thoughts?
Utilizing the Shape Tracer (in conjunction with #Bertrand's assistance in the comments above) helped me get to where I need. Here is the final layout code I went with (which has some super hacky stuff in it):
#using Orchard.Utility.Extensions;
#using System.Linq
#functions
{
public class PressRelease
{
public PressRelease()
{
this.Attachments = new List<Attachment>();
}
private string _NavigateUrl = string.Empty;
public string Title { get; set; }
public string Source { get; set; }
public DateTime PublishDate { get; set; }
public string Body { get; set; }
public List<Attachment> Attachments { get; set; }
public string NavigateUrl
{
get { return string.Format("{0}://{1}/{2}", HttpContext.Current.Request.Url.Scheme, HttpContext.Current.Request.Url.Authority, _NavigateUrl); }
set { this._NavigateUrl = value; }
}
}
public class Attachment
{
public string Filename { get; set; }
public string Path { get; set; }
}
}
#{
//add list of dynamic objects to strongly typed class
var releases = new List<PressRelease>();
foreach (var item in #Model.Items)
{
var release = new PressRelease
{
Title = item.ContentItem.TitlePart.Title,
Source = item.ContentItem.PressRelease.Source.Value,
PublishDate = item.ContentItem.PressRelease.Date.DateTime,
//this is super hacky to get a chopped version of the HTML submitted for a summary
Body = item.ContentItem.BodyPart.Text,
NavigateUrl = item.ContentItem.AutoroutePart.Path
};
//load attachment(s) to class
var attachments = (Orchard.MediaLibrary.Fields.MediaLibraryPickerField)item.ContentItem.PressRelease.Attachment;
if (attachments.MediaParts.Count() > 0)
{
foreach (var part in attachments.MediaParts)
{
release.Attachments.Add(new Attachment { Filename = part.FileName, Path = part.MediaUrl });
}
}
releases.Add(release);
}
}
#{
foreach (var item in releases)
{
<div class="press-release">
<div class="press-release-title">#item.Title</div>
<div class="press-release-meta">
<span class="press-release-source">Source: #item.Source</span>
#if (item.PublishDate != DateTime.MinValue)
{
<span class="press-release-date">#item.PublishDate.ToShortDateString()</span>
}
</div>
#if (item.Attachments.Count() > 0)
{
<div class="press-release-attachments">
<span class="press-release-attachments-title">Attached: </span>
#foreach (var attachment in item.Attachments)
{
#attachment.Filename
if (attachment != item.Attachments.Last())
{
<span>, </span>
}
}
</div>
}
<div class="press-release-body">
#{
var body = new HtmlString(Html.Excerpt(item.Body, 200).ToString().Replace(Environment.NewLine, "</p>" + Environment.NewLine + "<p>"));
<p>#body (read more)</p>
}
</div>
</div>
<div class="social">
<div class="fb-share-button" data-href="#item.NavigateUrl" data-type="button_count"></div>
</div>
if (item != releases.Last())
{
<hr />
}
}
}

Orchard Custom Settings Not Persisting

I'm pulling my hair out on this one; it should be so simple yet I can't figure out the issue.
I'm trying to simply save some custom settings in my module. I used the Orchard.Email module as an example on how to plug into the 'Settings' menu; my code is as follows:
Migrations.cs
public class CustomSettingsMigrations : DataMigrationImpl {
public int Create() {
SchemaBuilder.CreateTable("CustomSettingsPartRecord", table => table
.ContentPartRecord()
.Column<string>("GatewayUrl")
.Column<string>("MerchantId")
.Column<string>("MerchantPassword")
.Column<bool>("SandboxMode")
.Column<string>("SandboxGatewayUrl")
.Column<string>("SandboxMerchantId")
.Column<string>("SandboxMerchantPassword")
);
return 1;
}
}
Models/CustomSettingsPartRecord.cs
public class CustomSettingsPartRecord : ContentPartRecord {
public virtual string GatewayUrl { get; set; }
public virtual string MerchantId { get; set; }
public virtual string MerchantPassword { get; set; }
public virtual bool SandboxMode { get; set; }
public virtual string SandboxGatewayUrl { get; set; }
public virtual string SandboxMerchantId { get; set; }
public virtual string SandboxMerchantPassword { get; set; }
public CustomSettingsPartRecord() {
SandboxMode = true;
}
}
Models/CustomSettingsPart.cs
public class CustomSettingsPart : ContentPart<CustomSettingsPartRecord> {
private readonly ComputedField<string> _password = new ComputedField<string>();
public ComputedField<string> PasswordField {
get { return _password; }
}
public string GatewayUrl {
get { return Record.GatewayUrl; }
set { Record.GatewayUrl = value; }
}
public string MerchantId {
get { return Record.MerchantId; }
set { Record.MerchantId = value; }
}
public string MerchantPassword {
get { return Record.MerchantPassword; }
set { Record.MerchantPassword = value; }
}
public bool SandboxMode {
get { return Record.SandboxMode; }
set { Record.SandboxMode = value; }
}
public string SandboxGatewayUrl {
get { return Record.SandboxGatewayUrl; }
set { Record.SandboxGatewayUrl = value; }
}
public string SandboxMerchantId {
get { return Record.SandboxMerchantId; }
set { Record.SandboxMerchantId = value; }
}
public string SandboxMerchantPassword {
get { return Record.SandboxMerchantPassword; }
set { Record.SandboxMerchantPassword = value; }
}
public bool IsValid() {
return ((!String.IsNullOrWhiteSpace(Record.GatewayUrl)
&& !String.IsNullOrWhiteSpace(Record.MerchantId)) ||
(Record.SandboxMode && !String.IsNullOrWhiteSpace(Record.SandboxGatewayUrl) &&
!String.IsNullOrWhiteSpace(Record.SandboxMerchantId)));
}
}
Handlers/CustomSettingsPartHandler.cs
[UsedImplicitly]
public class CustomSettingsPartHandler : ContentHandler {
private readonly IEncryptionService _encryptionService;
public CustomSettingsPartHandler(IRepository<CustomSettingsPartRecord> repository, IEncryptionService encryptionService) {
T = NullLocalizer.Instance;
Logger = NullLogger.Instance;
_encryptionService = encryptionService;
Filters.Add(new ActivatingFilter<CustomSettingsPart>("Site"));
Filters.Add(StorageFilter.For(repository));
OnLoaded<CustomSettingsPart>(LazyLoadHandlers);
}
public Localizer T { get; set; }
public new ILogger Logger { get; set; }
void LazyLoadHandlers(LoadContentContext context, CustomSettingsPart part) {
part.PasswordField.Getter(() => {
try {
return String.IsNullOrWhiteSpace(part.Record.MerchantPassword) ? String.Empty : Encoding.UTF8.GetString(_encryptionService.Decode(Convert.FromBase64String(part.Record.MerchantPassword)));
}
catch (Exception) {
Logger.Error("The merchant password could not be decrypted. It might be corrupt, try to reset it.");
return null;
}
});
part.PasswordField.Setter(value => part.Record.MerchantPassword = String.IsNullOrWhiteSpace(value) ? String.Empty : Convert.ToBase64String(_encryptionService.Encode(Encoding.UTF8.GetBytes(value))));
}
protected override void GetItemMetadata(GetContentItemMetadataContext context) {
if (context.ContentItem.ContentType != "Site")
return;
base.GetItemMetadata(context);
context.Metadata.EditorGroupInfo.Add(new GroupInfo(T("Custom")));
}
}
Drivers/CustomSettingsPartDriver.cs
public class CustomSettingsPartDriver : ContentPartDriver<CustomSettingsPart> {
private const string TemplateName = "Parts/CustomSettings";
public CustomSettingsPartDriver() {
T = NullLocalizer.Instance;
}
public Localizer T { get; set; }
protected override string Prefix { get { return "CustomSettings"; } }
protected override DriverResult Editor(CustomSettingsPart part, dynamic shapeHelper) {
return ContentShape("Parts_CustomSettings_Edit",
() => shapeHelper.EditorTemplate(TemplateName: TemplateName, Model: part, Prefix: Prefix))
.OnGroup("custom");
}
protected override DriverResult Editor(CustomSettingsPart part, IUpdateModel updater, dynamic shapeHelper) {
return ContentShape("Parts_CustomSettings_Edit", () => {
var previousPassword = part.MerchantPassword;
updater.TryUpdateModel(part, Prefix, null, null);
// restore password if the input is empty, meaning it has not been changed
if (String.IsNullOrEmpty(part.MerchantPassword)) {
part.MerchantPassword = previousPassword;
}
return shapeHelper.EditorTemplate(TemplateName: TemplateName, Model: part, Prefix: Prefix)
.OnGroup("custom");
});
}
}
Views/EditorTemplates/Parts/CustomSettings.cshtml
#model CustomModule.Models.CustomSettingsPart
#{
Script.Require("jQuery");
}
<fieldset>
<legend>#T("Custom Settings")</legend>
<div>
<label for="#Html.FieldIdFor(m => m.GatewayUrl)">#T("Gateway Url")</label>
#Html.EditorFor(m => m.GatewayUrl)
#Html.ValidationMessage("GatewayUrl", "*")
</div>
<div>
<label for="#Html.FieldIdFor(m => m.MerchantId)">#T("Merchant ID")</label>
#Html.EditorFor(m => m.MerchantId)
#Html.ValidationMessage("MerchantId", "*")
</div>
<div>
<label for="#Html.FieldIdFor(m => m.MerchantPassword)">#T("Merchant Password")</label>
#Html.PasswordFor(m => m.MerchantPassword)
#Html.ValidationMessage("MerchantPassword", "*")
</div>
<div>
#Html.EditorFor(m => m.SandboxMode)
<label for="#Html.FieldIdFor(m => m.SandboxMode)" class="forcheckbox">#T("Enable Sandbox Mode (for testing)")</label>
#Html.ValidationMessage("SandboxMode", "*")
</div>
<div id="sandboxSettings">
<div>
<label for="#Html.FieldIdFor(m => m.SandboxGatewayUrl)">#T("Sandbox Gateway Url")</label>
#Html.EditorFor(m => m.SandboxGatewayUrl)
#Html.ValidationMessage("SandboxGatewayUrl", "*")
</div>
<div>
<label for="#Html.FieldIdFor(m => m.SandboxMerchantId)">#T("Sandbox Merchant ID")</label>
#Html.EditorFor(m => m.SandboxMerchantId)
#Html.ValidationMessage("SandboxMerchantId", "*")
</div>
<div>
<label for="#Html.FieldIdFor(m => m.SandboxMerchantPassword)">#T("Sandbox Merchant Password")</label>
#Html.EditorFor(m => m.SandboxMerchantPassword)
#Html.ValidationMessage("SandboxMerchantPassword", "*")
</div>
</div>
</fieldset>
#using (Script.Foot()) {
<script>
$('##Html.FieldIdFor(m => m.SandboxMode)').on('click', function() {
$('#sandboxSettings').toggle($(this).prop('checked'));
});
</script>
}
I have the Placement.info and I can access the View through the "Custom" menu item underneath "Settings" in the main menu. The View loads fine, and when I enter some details and click 'Save', the form is sent find and will hit the CustomSettingsPartDriver.cs DriverResult Editor(CustomSettingsPart part, IUpdateModel updater.. method.
I believe this is where the issue could be, as it doesn't hit any breakpoints inside the return ContentShape("Parts_CustomSettings_Edit, () => { lambda expression.
Could anyone shed any light on how I can resolve this? I'm sure it's a simple issue, but I've been trying to figure this one out for a while unsuccessfully. Thanks!
Okay, I managed to figure this one out. I feel silly asking questions then answering them myself, but if it saves one person time in the future, then it's worth it.
The issue was, as I expected, in the Driver. I appended the .OnGroup() extension to the wrong Shape.
Below is the fixed Driver code:
protected override DriverResult Editor(CustomSettingsPart part, IUpdateModel updater, dynamic shapeHelper) {
return ContentShape("Parts_CustomSettings_Edit", () => {
var previousPassword = part.MerchantPassword;
updater.TryUpdateModel(part, Prefix, null, null);
// restore password if the input is empty, meaning it has not been changed
if (String.IsNullOrEmpty(part.MerchantPassword)) {
part.MerchantPassword = previousPassword;
}
return shapeHelper.EditorTemplate(TemplateName: TemplateName, Model: part, Prefix: Prefix);
// Offending extension -> .OnGroup("custom");
}).OnGroup("custom"); // In the correct location
}
*facepalm*

Unable to Update Many to Many Relationship. Entity Framework Code First

I am trying to update a many to many relationship that I have setup in Entity Framework using Code First. I've created the following Models.
[Serializable]
public class ClientFormField : FormField
{
public ClientFormField()
{
CanDisable = true;
}
public virtual ClientFormGroup Group { get; set; }
public virtual ICollection<ClientValue> Values { get; set; }
public virtual ICollection<LetterTemplate> LetterTemplates { get; set; }
}
[Serializable]
public class CaseFormField : FormField
{
public CaseFormField()
{
CanDisable = true;
}
public virtual CaseFormGroup Group { get; set; }
public virtual ICollection<LetterTemplate> LetterTemplates { get; set; }
public override IEnumerable<ValidationResult> Validate(ValidationContext validationContext)
{
foreach (var val in base.Validate(validationContext))
yield return val;
}
}
[Serializable]
public class SystemField : TrackableEntity
{
public string Name { get; set; }
public string Value { get; set; }
public string VarName { get; set; }
public virtual SystemFieldType SystemFieldType { get; set; }
public int TypeId { get; set; }
public ICollection<LetterTemplate> LetterTemplates { get; set; }
public override IEnumerable<ValidationResult> Validate(ValidationContext validationContext)
{
if (string.IsNullOrWhiteSpace(Name))
yield return new ValidationResult("System field must have a name.", new[] { "SystemFieldName" });
if (string.IsNullOrWhiteSpace(Value))
yield return new ValidationResult("System field must have a value.", new[] { "SystemFieldValue" });
var regex = new Regex(#"^[a-zA-Z0-9-_]+$");
if (!string.IsNullOrWhiteSpace(VarName) && !regex.IsMatch(VarName))
yield return
new ValidationResult("Varname can only contain alphanumeric, underscore, or hyphen",
new[] { "SystemFieldVarName" });
if (TypeId <= 0)
yield return new ValidationResult("System Field must have a type.", new[] { "SystemFieldType" });
}
}
[Serializable]
public class LetterTemplate : TrackableEntity
{
public LetterTemplate()
{
ClientFields = new Collection<ClientFormField>();
CaseFields = new Collection<CaseFormField>();
SystemFields = new Collection<SystemField>();
}
public string Name { get; set; }
public string Data { get; set; }
public virtual ICollection<ClientFormField> ClientFields { get; set; }
public virtual ICollection<CaseFormField> CaseFields { get; set; }
public virtual ICollection<SystemField> SystemFields { get; set; }
public override IEnumerable<ValidationResult> Validate(ValidationContext validationContext)
{
if(string.IsNullOrWhiteSpace(Name))
yield return new ValidationResult("Form Template must have a name", new[] { "Name" });
if(string.IsNullOrWhiteSpace(Data))
yield return new ValidationResult("Form Template must have content", new[] { "Data" });
}
}
Below is the configuration for the LetterTemplate class.
public class LetterTemplateConfiguration : BaseTrackableEntityConfiguration<LetterTemplate>
{
public LetterTemplateConfiguration()
{
HasMany(c => c.ClientFields).WithMany(c => c.LetterTemplates)
.Map(m =>
{
m.MapLeftKey("LetterTemplateId");
m.MapRightKey("ClientFormFieldId");
m.ToTable("LetterTemplateClientFields");
});
HasMany(c => c.CaseFields).WithMany(c => c.LetterTemplates)
.Map(m =>
{
m.MapLeftKey("LetterTemplateId");
m.MapRightKey("CaseFormFieldId");
m.ToTable("LetterTemplateCaseFields");
});
HasMany(c => c.SystemFields).WithMany(c => c.LetterTemplates)
.Map(m =>
{
m.MapLeftKey("LetterTemplateId");
m.MapRightKey("SystemFieldId");
m.ToTable("LetterTemplateSystemFields");
});
}
}
Here is the controller method for Add/Update and the server method that holds the business logic for Add/Update
[HttpPost]
[ValidateAntiForgeryToken]
[ValidateInput(false)]
public ActionResult Manage(LetterTemplate template)
{
if(ModelState.IsValid)
{
if (_letterTemplateService.Save(template) != null)
return RedirectToAction("List");
}
ViewBag.ClientFields = _clientFieldService.GetAllFields().OrderBy(f => f.Name);
ViewBag.CaseFields = _caseFieldService.GetAllFields().OrderBy(f => f.Name);
ViewBag.SystemFields = _systemFieldService.GetAllFields().OrderBy(f => f.Name);
return View(template);
}
public LetterTemplate Save(LetterTemplate template)
{
var dbTemplate = template;
if (template.Id > 0)
{
dbTemplate = _letterTemplateRepo.GetById(template.Id);
dbTemplate.Name = template.Name;
dbTemplate.Data = template.Data;
}
dbTemplate.ClientFields.Clear();
foreach (var field in _clientFieldRepo.All().Where(field => template.Data.Contains("~~" + field.VarName + "~~")))
dbTemplate.ClientFields.Add(field);
dbTemplate.CaseFields.Clear();
foreach (var field in _caseFieldRepo.All().Where(field => template.Data.Contains("~~" + field.VarName + "~~")))
dbTemplate.CaseFields.Add(field);
dbTemplate.SystemFields.Clear();
foreach (var field in _systemFieldRepo.All().Where(field => template.Data.Contains("~~" + field.VarName + "~~")))
dbTemplate.SystemFields.Add(field);
return template.Id <= 0 ? _letterTemplateRepo.Add(dbTemplate) : _letterTemplateRepo.Update(dbTemplate);
}
Here is the view for Add/Update of the Letter Template.
#section RightContent
{
<h4>Manage</h4>
<form method="POST" action="/LetterTemplate/Manage" id="templateForm">
<div id="toolbar">
<div style="float: left;padding: 3px 0px 0px 10px;">
#Html.LabelFor(m => m.Name)
#Html.TextBoxFor(m => m.Name)
</div>
<div class="item" onclick=" $('#templateForm').submit(); ">
<span class="save"></span>Save
</div>
<div class="item" onclick=" window.location = '/LetterTemplate/List'; ">
<span class="list"></span>Back To List
</div>
</div>
<div class="formErrors">
#Html.ValidationSummary()
</div>
#Html.HiddenFor(m => m.Id)
#Html.HiddenFor(m => m.Active)
#Html.HiddenFor(m => m.IsDeleted)
#Html.TextAreaFor(m => m.Data)
#Html.AntiForgeryToken()
</form>
}
When I am creating a new template from the view everything is working fine. I get fields populated in my Many to Many relationship tables as I expect. When I attempt to update the relationship which should clear out all existing relations and create new relations nothing happens. The tables are not affected at all. I've read through several different posts about issues with updating many to many tables but haven't found anything that fixes my issue. The is the first time I have attempted many to many with EF code first and followed many tutorials before hand but it seems that no matter what I do, EF will not update the relationship tables.
UPDATE:
Queries generated when adding a new template:
DECLARE #0 nvarchar = N'Test',
#1 nvarchar = N'<p>~~case_desc~~</p>
<p>~~fname~~</p>
<p>~~lname~~</p>
',
#2 bit = 1,
#3 bit = 0,
#4 int = 2,
#5 int = 2,
#6 DateTime2 = '2013-04-08T16:36:09',
#7 DateTime2 = '2013-04-08T16:36:09'
insert [dbo].[LetterTemplates]([Name], [Data], [Active], [IsDeleted], [CreatedById], [ModifiedById], [DateCreated], [DateModified])
values (#0, #1, #2, #3, #4, #5, #6, #7)
DECLARE #0 int = 2,
#1 int = 1
insert [dbo].[LetterTemplateClientFields]([LetterTemplateId], [ClientFormFieldId])
values (#0, #1)
DECLARE #0 int = 2,
#1 int = 2
insert [dbo].[LetterTemplateClientFields]([LetterTemplateId], [ClientFormFieldId])
values (#0, #1)
DECLARE #0 int = 2,
#1 int = 3
insert [dbo].[LetterTemplateClientFields]([LetterTemplateId], [ClientFormFieldId])
values (#0, #1)
Query Generated on update:
DECLARE #0 nvarchar = N'Test',
#1 nvarchar = N'<p>~~case_desc~~</p>
<p> </p>
<p>~~fname~~</p>
<p> </p>
<p>~~dob~~</p>
',
#2 bit = 1,
#3 bit = 0,
#4 int = 2,
#5 int = 2,
#6 DateTime2 = '2013-04-08T16:23:12',
#7 DateTime2 = '2013-04-08T16:33:15',
#8 int = 1
update [dbo].[LetterTemplates]
set [Name] = #0, [Data] = #1, [Active] = #2, [IsDeleted] = #3, [CreatedById] = #4, [ModifiedById] = #5, [DateCreated] = #6, [DateModified] = #7
where ([Id] = #8)
UPDATE
My repository pattern has 2 base generic classes. A Base Trackable Entity Repository and a Base Repository. The base trackable entity repo handles making sure deleted items are soft deleted, getting non deleted items, and managing the createdby/modifiedby and createdDate/UpdatedDate. The base repo handles the rest of the basic CRUD operations. Below is the update method and associated methods that get called when I call update through the LetterTemplateRepository. Since this repo inherits the base trackable entity repo it runs update from the base class.
public override T Update(T entity)
{
return Update(entity, false);
}
public override T Update(T entity, bool attachOnly)
{
InsertTeData(ref entity);
entity.ModifiedById = CurrentUserId;
entity.DateModified = DateTime.Now;
_teDB.Attach(entity);
_db.SetModified(entity);
if (!attachOnly) _db.Commit();
return entity;
}
private void InsertTeData(ref T entity)
{
if (entity == null || entity == null) return;
var dbEntity = GetById(entity.Id);
if (dbEntity == null) return;
_db.Detach(dbEntity);
entity.CreatedById = dbEntity.CreatedById;
entity.DateCreated = dbEntity.DateCreated;
entity.ModifiedById = dbEntity.ModifiedById;
entity.DateModified = dbEntity.DateModified;
}
The SetModified method in by DbContext just sets the EntityState to Modified. I use a Fake DbContext and DbSet in my unit tests so any EF specific calls I extend through the DbContext to allow my tests to work without having to create a bunch of Fake Repositories.
Turns out the issue was in the InsertTeData Method. When it was detaching the entity that I pulled from the db to make sure createdby and created date it caused the entity I was working with to lose all information about the many to many relationships. My guessing is the way the entity tracking works and they both had the same key.
I've removed the InsertTeData method and now manage everything as default values in the constructor of the abstract TrackableEntity class and everything is working now.
public override T Update(T entity, bool attachOnly)
{
entity.ModifiedById = CurrentUserId;
entity.DateModified = DateTime.Now;
_teDB.Attach(entity);
_db.SetModified(entity);
if (!attachOnly) _db.Commit();
return entity;
}
After running all my unit tests, integration tests, and some manual tests, everything passed so I am fine with this change.

Resources