skip to Main Content

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


  1. Chosen as BEST ANSWER

    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 call DataBind() 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

    <%@ Page Title="Home Page" Language="C#" AutoEventWireup="true" Inherits="ModelExample._Default" CodeBehind="~/Default.aspx.cs" %>
    
    <html>
    <head><title>Vet Example</title></head>
    <body>
        <form runat="server" id="MainForm">
            <!--Main form view shows just the 1:1 properties - not the list of notes-->
            <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>
                </EditItemTemplate>
            </asp:FormView>
            <!--Dedicated form view for adding a note. 
                Not visible by default - shown in main form view's select method if a record is found.-->
            <asp:FormView ID="frmVwNotes" runat="server" DefaultMode="Insert" Visible="false"
                EnableModelValidation="true" ItemType="Models.VisitNote"
                InsertMethod="frmVwNotes_InsertItem">
                <InsertItemTemplate>
                    <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" SelectedValue="<%# BindItem.NoteType %>"></asp:DropDownList>
                        </div>
                        <div>
                            <asp:TextBox runat="server" ID="tbNewNoteBody" TextMode="MultiLine"
                                Rows="5" Columns="50" Text="<%# BindItem.NoteBody %>"></asp:TextBox>
                        </div>
                        <div>
                            <asp:Button runat="server" ID="btnAddNote" Text="Add Note" CommandName="Insert" />
                        </div>
                    </div>
                </InsertItemTemplate>
            </asp:FormView>
            <!--Repeater shows readonly history of notes for selected pet-->
            <div id="NotesHistory">
                <asp:Repeater runat="server" ID="rptNotes" SelectMethod="rptNotes_GetData" ItemType="Models.VisitNote">
                    <HeaderTemplate>
                        <hr />
                        <h2>Notes Histroy:</h2>
                    </HeaderTemplate>
                    <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>
        </form>
    </body>
    </html>
    

    CODE BEHIND

    using Models;
    using System;
    using System.Collections.Generic;
    using System.Linq;
    using System.Web.ModelBinding;
    using System.Web.UI;
    
    namespace ModelExample
    {
        public partial class _Default : Page
        {
            private static List<PetModel> _pets = new List<PetModel>();
            protected void Page_Load(object sender, EventArgs e)
            {
                //populate static list of model objects if it hasn't already been done
                //this takes the place of database for our example
                if (!_pets.Any())
                {
                    var notesCollection = new List<VisitNote>()
                    {
                        new VisitNote() {
                            NoteId = 11, CreatedBy = "Jeanann Sutherland", CreatedOn = "2021-10-31",
                            NoteType = NoteTypes.Intake,
                            NoteBody = "Pekoe has presented with sneezing, enflamed nose, nasal drainage and abnormaly clingy demeanor."
                        }, new VisitNote()
                        {
                            NoteId = 12, CreatedBy = "M Jance (student)", CreatedOn="2022-01-11", NoteType = NoteTypes.Intake, NoteBody="Checkup"
                        }
                    };
                    _pets.Add(new PetModel()
                    {
                        PetId = 11,
                        DateOfBirth = "2014-07-24",
                        Name = "Pekoe",
                        Species = PetSpecies.Cat,
                        Weight = 12.3f,
                        VisitNotes = notesCollection
                    });
                }
            }
    
            public IEnumerable<string> ddlSpecies_Get()
            {
                return Enum.GetNames(typeof(PetSpecies)).ToList<string>();
            }
            public IEnumerable<string> NoteType_Get()
            {
                return Enum.GetNames(typeof(NoteTypes)).ToList<string>();
            }
            public PetModel frmVw_GetItem([QueryString] int? PetId)
            {
                var result = _pets.FirstOrDefault(p => p.PetId == PetId);
                frmVwNotes.Visible = (result != null);
                return result;
            }
            public void frmVw_UpdateItem(int? PetId)
            {
                PetModel item = _pets.FirstOrDefault(p => p.PetId == PetId);
                if (item == null)
                {
                    // The item wasn't found
                    ModelState.AddModelError("", String.Format("Item with id {0} was not found", PetId));
                    return;
                }
                TryUpdateModel(item);
                // visit notes are no longer null here - I suspect that binding them to the nested
                //repeater was causing this to happen.
                var notesCount = item.VisitNotes?.Count() ?? 0;
            }
    
            public void frmVwNotes_InsertItem([QueryString] int? PetId)
            {
                var item = new VisitNote();
                TryUpdateModel(item);
                if (ModelState.IsValid)
                {
                    // Save changes here
                    item.CreatedOn = DateTime.Now.ToString("G");
                    item.NoteId = _pets.Select(p => p.VisitNotes.Select(n => n.NoteId).Max()).ToList<int>().Max() + 1;
                    //normally we'd get the author from the logged-in user
                    item.CreatedBy = "Dr Example";
                    var pet = _pets.Where(p => p.PetId == PetId).First();
                    pet.VisitNotes.Add(item);
                    //call databind on the notes repeater for the new note to display
                    rptNotes.DataBind();
                }
            }
    
            public System.Collections.IEnumerable rptNotes_GetData([QueryString] int? PetId)
            {
                return _pets.Where(p => p.PetId == PetId).FirstOrDefault()?.VisitNotes;
            }
        }
    
    }
    
    
    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
        }
    }
    

    1. The ddlNewNoteType and tbNewNoteBody are jus part of frmVw. Clicking on btnAddNote 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" />

    1. In frmVw_UpdateItem, you successfully get the item. Please, note that at that point, VisitNotes does exist, so you may grab it for later use:
    PetModel item = _pets.FirstOrDefault(p => p.PetId == PetId);
    var itemVisitNotes = item.VisitNotes;
    
    1. TryUpdateModel does update the item with the values provided by the bound fields of the FormView BUT VisitNotes is not one of them since it is not really bound to the form; it is bound to the repeater.

    2. Nevertheless, you may access the values of ddlNewNoteType and tbNewNoteBody to add a new note (if any) to the item’s list of notes. As said in (3), it is null at that point but you can use itemVisitsNotes from (2) to initialize it:

    if (ModelState.IsValid)
    {
        item.VisitNotes = itemVisitNotes;
        item.VisitNotes.Add(new VisitNote()
        {                    
            CreatedBy = "Jeanann Sutherland",
            CreatedOn = DateTime.Now.ToShortDateString(),
            NoteType = NoteTypes.Intake,
            NoteBody = (frmVw.FindControl("tbNewNoteBody") as TextBox).Text,
        });
    }
    
    1. One last thing to note in (4) is the use of FindControl to access the controls of the FormView (as a sample, I’ve accessed tbNewNoteBody 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:

    public void frmVw_UpdateItem(int PetId)
    {
        PetModel item = _pets.FirstOrDefault(p => p.PetId == PetId);
        var itemVisitNotes = item.VisitNotes;
    
        if (item == null)
        {
            // The item wasn't found
            ModelState.AddModelError("", String.Format("Item with id {0} was not found", PetId));
            return;
        }
        TryUpdateModel(item);
        if (ModelState.IsValid)
        {
            item.VisitNotes = itemVisitNotes;
            item.VisitNotes.Add(new VisitNote()
            {                    
                CreatedBy = "Jeanann Sutherland",
                CreatedOn = DateTime.Now.ToShortDateString(),
                NoteType = NoteTypes.Intake,
                NoteBody = (frmVw.FindControl("tbNewNoteBody") as TextBox).Text,
            });
        }
    }
    

    Hope that helps.

    Login or Signup to reply.
Please signup or login to give your own answer.
Back To Top
Search