In an asp.net webforms 4.8 project I’m using 2 way model binding (to a FormView). So far things work great for my model’s scalar properties and Enums but things are not working well with my primary model’s properties that aren’t simple scalar values (like lists of other objects). I have 3 primary issues/questions.
My SelectMethod returns a model which has as a property a List< AnotherCustomClass>. Using a repeater this property is rendered in the EditView (so long as AnotherCustomClass is marked [Serializable]) but in my UpdateMethod I run into issue (1) – that property always ends up null.
I’ve also created a button for adding another item in the List but from the code behind method I don’t know how to (2) reference my model instance to add the new item. Issue (3) is that once that method runs I don’t know how to cause the repeater to render again to see my new item. (ideally this could be done without losing any other unsaved changes to my model.)
Here is a simplified example demonstrating my issues. I’ve spent days on these three issues and would really appreciate being pointed in the right direction as I assume I’m simply dealing with this property in the wrong way.
ASPX
<%@ Page Title="Home Page" Language="C#" AutoEventWireup="true" Inherits="ModelExample._Default" CodeBehind="~/Default.aspx.cs" %>
<html>
<head></head>
<body>
<form runat="server" id="MainForm">
<asp:FormView ID="frmVw" runat="server" DefaultMode="Edit"
EnableModelValidation="true" ItemType="Models.PetModel"
DataKeyNames="PetId"
SelectMethod="frmVw_GetItem" UpdateMethod="frmVw_UpdateItem">
<EmptyDataTemplate>
No pet found, try adding ?PetId=11 as a query string parameter.
</EmptyDataTemplate>
<EditItemTemplate>
<div id="EditPetDetails">
<div><h2>Pet Details:</h2></div>
<div>
<asp:Label runat="server" ID="lblPetName" Text="Name"></asp:Label>
<asp:TextBox runat="server" ID="tbPetName" Text="<%# BindItem.Name %>"></asp:TextBox>
</div>
<div>
<asp:Label runat="server" ID="lblDob" Text="Date of Birth"></asp:Label>
<asp:TextBox runat="server" ID="tbDob" Text='<%# BindItem.DateOfBirth %>' TextMode="Date"></asp:TextBox>
</div>
<div>
<asp:Label runat="server" ID="lblSpecies" Text="Species"></asp:Label>
<asp:DropDownList runat="server" ID="ddlSpecies" SelectMethod="ddlSpecies_Get" SelectedValue='<%# BindItem.Species %>'></asp:DropDownList>
</div>
<div>
<asp:Label runat="server" ID="lblWeight" Text="Weight (kg)"></asp:Label>
<asp:TextBox runat="server" ID="tbWeight" Text="<%# BindItem.Weight %>" TextMode="Number"></asp:TextBox>
</div>
<div>
<asp:Button runat="server" ID="btnSave" Text="Save" CommandName="Update" />
</div>
</div>
<hr />
<div id="AddNotes">
<div>
<h2>Add Note:</h2>
</div>
<div>
<asp:Label runat="server" ID="lblNewNoteType" Text="Type"></asp:Label>
<asp:DropDownList runat="server" ID="ddlNewNoteType" SelectMethod="NoteType_Get"></asp:DropDownList>
</div>
<div>
<asp:TextBox runat="server" ID="tbNewNoteBody" TextMode="MultiLine"
Rows="5" Columns="50"></asp:TextBox>
</div>
<div>
<asp:Button runat="server" ID="btnAddNote" Text="Add Note" OnClick="btnAddNote_Click" />
</div>
</div>
<hr />
<div id="NotesHistory">
<h2>Notes Histroy:</h2>
<asp:Repeater runat="server" ID="rptNotes" ItemType="Models.VisitNote" DataSource="<%# BindItem.VisitNotes %>">
<ItemTemplate>
<div>
<asp:Label runat="server" ID="lblNoteCreatedOn" Text="Date:"></asp:Label>
<asp:TextBox runat="server" ID="tbNoteCreatedOne" Text="<%# Item.CreatedOn %>" TextMode="Date" ReadOnly="true"></asp:TextBox>
</div>
<div>
<asp:Label runat="server" ID="lblNoteCreatedBy" Text="Author:"></asp:Label>
<asp:Label runat="server" ID="tbNoteCreatedBy" Text="<%# Item.CreatedBy %>"></asp:Label>
</div>
<div>
<asp:Label runat="server" ID="lblNoteType" Text="Type:"></asp:Label>
<asp:Label runat="server" ID="tbNoteType" Text="<%# Item.NoteType %>"></asp:Label>
</div>
<div>
<asp:TextBox runat="server" ID="tbNoteBody" Text="<%# Item.NoteBody %>"
ReadOnly="true" TextMode="MultiLine" Rows="5" Columns="50"></asp:TextBox>
</div>
</ItemTemplate>
</asp:Repeater>
</div>
</EditItemTemplate>
</asp:FormView>
</form>
</body>
</html>
Code Behind
using Models;
using System;
using System.CodeDom;
using System.Collections.Generic;
using System.Linq;
using System.Web;
using System.Web.ModelBinding;
using System.Web.UI;
using System.Web.UI.WebControls;
namespace ModelExample
{
public partial class _Default : Page
{
private static List<PetModel> _pets = new List<PetModel>();
protected void Page_Load(object sender, EventArgs e)
{
if (!_pets.Any())
{
var notesCollection = new List<Models.VisitNote>()
{
new Models.VisitNote() {
NoteId = 11, CreatedBy = "Jeanann Sutherland", CreatedOn = "2021-10-31",
NoteType = Models.NoteTypes.Intake,
NoteBody = "Pekoe has presented with sneezing, enflamed nose, nasal drainage and abnormaly clingy demeanor."
}
};
_pets.Add(new Models.PetModel()
{
PetId = 11,
DateOfBirth = "2014-07-24",
Name = "Pekoe",
Species = Models.PetSpecies.Cat,
Weight = 12.3f,
VisitNotes = notesCollection
});
}
}
public IEnumerable<string> ddlSpecies_Get()
{
return Enum.GetNames(typeof(Models.PetSpecies)).ToList<string>();
}
public IEnumerable<string> NoteType_Get()
{
return Enum.GetNames(typeof(Models.NoteTypes)).ToList<string>();
}
public Models.PetModel frmVw_GetItem([QueryString] int? PetId)
{
return _pets.FirstOrDefault(p=> p.PetId == PetId);
}
// The id parameter name should match the DataKeyNames value set on the control
public void frmVw_UpdateItem(int? PetId)
{
Models.PetModel item = _pets.FirstOrDefault(p=> p.PetId == PetId);
// Load the item here, e.g. item = MyDataLayer.Find(id);
if (item == null)
{
// The item wasn't found
ModelState.AddModelError("", String.Format("Item with id {0} was not found", PetId));
return;
}
TryUpdateModel(item);
// when updating the Notes property is null - where did the notes go?
var notesCount = item.VisitNotes?.Count() ?? 0;
}
protected void btnAddNote_Click(object sender, EventArgs e)
{
//Desired behavior is - when user adds note the note is added to the VisitNote list of the model being edited
//The repeater that displays the list should show the new note
//The note sits in the model - logic in the Update method will find notes with default ID and insert as needed.
//author info comes from logged in user
var author = "Dr P Sorthes";
//How do I pull the selected value from ddlNewNoteType and tbNewNoteBody ?
//Googling find people casting a control found by calling the formview's find control method
//is that really the right/best way to do this?
var noteType = NoteTypes.Diagnosis;
var body = "Where is the body?";
//How do I get a reference to the current model the page is using?
//if you debug at this point you'll see frmVw's DataItem and DataItemContainer are both null
//though DataTimeCount is 1
var pet = _pets.Where(p => p.PetId == 11).First();
var oldNotes = pet.VisitNotes;
//Try update throws:
//System.InvalidOperationException: ''TryUpdateModel' must be passed a value provider or alternatively must be invoked
//from inside a data-operation method of a control that uses model
//TryUpdateModel(pet);
pet.VisitNotes = oldNotes ?? new List<VisitNote>();
pet.VisitNotes.Add(new VisitNote() { CreatedBy = author, NoteType = noteType, CreatedOn = DateTime.UtcNow.ToString("yyyy-MM-dd"), NoteBody = body });
//What do I do to make the notes repeater show the newly added note?
}
}
}
namespace Models
{
public class PetModel
{
private DateTime? dateOfBirth;
public int PetId { get; set; }
public string Name { get; set; }
public string DateOfBirth { get { return dateOfBirth?.ToString("yyyy-MM-dd") ?? ""; } set { dateOfBirth = DateTime.Parse(value); } }
public PetSpecies Species { get; set; }
public Single Weight { get; set; }
public List<VisitNote> VisitNotes { get; set; }
}
[Serializable]
public class VisitNote
{
private DateTime? createdOn;
public int NoteId { get; set; }
public String CreatedOn { get { return createdOn?.ToString("yyyy-MM-dd") ?? ""; } set { createdOn = DateTime.Parse(value); } }
public String CreatedBy { get; set; }
public NoteTypes NoteType { get; set; }
public String NoteBody { get; set; }
}
[Serializable]
public enum NoteTypes
{
Intake,
Diagnosis,
Prescription,
Followup
}
[Serializable]
public enum PetSpecies
{
Dog,
Cat,
Bird,
Fish,
Snake
}
}
2
Answers
No other answers yet, so I'm posting the best option I've come up with in case anyone else trying to figure this out reads this later.
I'm still hoping that I can find a way to accomplish what I was asking for within a single formview.
I can get close if instead I use a formview (in edit mode) for the simple properties of the model, a second formview after the first (in insert mode) for a new
VistNote
, and a repeater after the second formview to display the history of existing notes. In the insert method of the second formview callDataBind()
on the repeater so the newly inserted note is displayed. I've confirmed that unsaved edits in the first formview survive all of the above.It's two formviews and a repeater instead of a single formview and not quite as clean as I had envisioned but it does indeed work.
ASPX
CODE BEHIND
ddlNewNoteType
andtbNewNoteBody
are jus part offrmVw
. Clicking onbtnAddNote
should be an update of the bound item -simply just for adding a new note. So,btnAddNote
becomes:<asp:Button runat="server" ID="btnAddNote" Text="Add Note" CommandName="Update" />
frmVw_UpdateItem
, you successfully get the item. Please, note that at that point,VisitNotes
does exist, so you may grab it for later use:TryUpdateModel
does update the item with the values provided by the bound fields of the FormView BUTVisitNotes
is not one of them since it is not really bound to the form; it is bound to the repeater.Nevertheless, you may access the values of
ddlNewNoteType
andtbNewNoteBody
to add a new note (if any) to the item’s list of notes. As said in (3), it isnull
at that point but you can useitemVisitsNotes
from (2) to initialize it:FindControl
to access the controls of the FormView (as a sample, I’ve accessedtbNewNoteBody
but you get the point, I am sure). Those controls are defined in a template (EditItemTemplate
) and that’s how things work in WebForms.So, all in all:
Hope that helps.