What is the right way to handle an asp webform save lifecycle (page_load fires before button event)?

71 Views Asked by At

It's years that I don't touch asp webforms and I got back into it with a legacy project. I couldn't remember exactly the page lifecycle but at my surprise, I built a form, with a submit button at the end of the form that saves on DB. The expected event order would have been:

First load of the page

  1. Page_Load -> I load form data (wheter it's empty or reading data from db and populate the form
  2. Button_event -> When I click to save on db if someone changes the form

What i expected when I try to save:

  1. Button event saves to db
  2. Page_load comes into play and i can refresh the form with new data

Of course I dusted my rust on ASP Webforms to discover that Page_Load always come first.

At this point I tought it was a good way to solve the problem by checking what button has been called and then in the IsPostback event manage the save and load of the form.

So in the Page_Load I would do:

if(IsPostBack){
    if(button is save){
      save data
    }
}

//Code to load db data into models
var data = getDataFromDb();
Control1.text = data.text1;
//etc etc...

My question is: Is this the correct way? what is the standard way to handle this?

I see also updatepanels are used but didn't want to get into too complicated stuff for a simple form.

1

There are 1 best solutions below

11
On

a good way to solve the problem by checking what button has been called and then in the IsPostback event manage the save and load of the form.

No, the instant you do that, is very same instant you quite much broken the whole idea of how the page is to work!!!

Page load:

It WILL fire on every post back. So, you have to make that assumption, and thus your code, your ideas, your thinking? It cannot matter, or better said should not matter.

In effect, say we do something REALLY simple, like in page load, fill out a grid, or dropdown list (combo box) or whatever?

Well, then ALL of that page setup, page load of data can only (should only) occur one time.

So, all of that code will be placed inside of the REAL first page load.

that is your if (!IsPostBack) code stub. In fact, out of the last 200 webform pages, 99% of them have that all important (!IsPostBack) stub.

If you don't do above? Then say a user selected a combo box value, and then you click some button on the page - (to say look at the combo box value). if on-load fires again, and you RE-load the combo box, you JUST lost the selection the user made on that page.

So, once you adopt the above concept (if (!IsPostBack) to setup the page, then you are NOW free to simple drop in a button, or even a drop-down list with auto post-back. You never have to care.

So, with above in mind????

You have a save button? Then write the code in the save button to save the data. this is really the ONLY way to build working web form pages.

But, you never care about page load.

Now, to be fair, a boatload of this confusing would have been eliminated if they had a actual event called FirstPage load, but we don't, and thus page load does double duty.

For the most part, and in most cases then, the page load event on additional post-backs should not matter, and you should not care, since it not going to do anything much of value. However, often there are some things that have to run each time - especially if you as a developer don't use the auto-"magic" view state that makes webforms oh so easy. So, in some cases, I will turn off view state (say for a drop down, or gridview - but then that means I DO HAVE to re-load that whatever content each time).

so, no, drop a button on the web form, double click on it, and you are now jumped to the "one little bit" of code that you can think about, and deal with. So, you are now free to write (and ONLY worry about that one nice little code stub). So, if that little code stub is to save data, then write the code to save the data, and you are done.

Keep in mind, that EVEN when you introduce a update panel, and thus don't have to post back the WHOLE page, but only update a small part of the web page (and not have to go to JavaScript and ajax school to achieve that ability to ONLY post-back and only update a small part of the web page? - Well, EVEN that cool update panel triggers the page load event each time!!!

So, from a developers point of view?

You drop buttons on the form, they each have their own code stub to do whatever the heck you want, and that makes/keeps the code VERY simple and easy to write.

But, this DOES mean you better not have code in the page load event that fires each time that re-loads data into say a dropdown list, or grid view. but, since one adopts the concept that the "one time" setup of values, loading of data ONLY ever occurs one time (in that all important if (!IsPostBack) code stub, then you really should never care, or worry or have to check which, or what button triggers the page life cycle, and the page load event thus will never matter anyway - it it only being used on FIRST page load to setup and load and pull data into the page on that first page load).

So, the assumption is that a page will often have many post backs from many different buttons (or even a drop down list with auto-postback). But, as such, it will not matter, since from the get go, the page is assumed to allow and have multiple post-backs occurring, and all of them do trigger the page load event each time, and will trigger page load BEFORE the button (or whatever) code stub for the given event THEN runs.

So, no, page load should not have to check, nor worry nor care about what button was clicked. You need some save code in a button, put that save code in that button stub, and it should just work.

So, in effect flip around what you have!

The "real" page load (!IsPostBack) stub in page load event is to load up the page data. Every other post back thus will not load that data again, including your save button and its code stub.

edit:

Lets take a really simple example. I will on page load fill out a drop down list, then after the user selects a value from drop down list, we display details about that selection.

So, the drop down list will be/have/execute a post-back. (and it could have been simple button - don't matter).

So, a dropdown, and some text boxes etc. to display that hotel information.

We have this markup:

enter image description here

(there is a bit more markup below, but it not important here).

So, on page load, we will load up the combo box. But, as noted, we can NOT load up the box each time on page load, since it would "over write" or "blow up" the selection we make each time for a post-back.

So, our page load even will look like this:

protected void Page_Load(object sender, EventArgs e)
{
    if (!IsPostBack)
        LoadCombo();
}


void LoadCombo()
{
    SqlCommand cmdSQL 
        = new SqlCommand("SELECT ID, HotelName FROM tblHotelsA ORDER BY HotelName");
    DropDownList1.DataSource = MyRstP(cmdSQL);
    DropDownList1.DataBind();
    // add extra please select option to drop down
    DropDownList1.Items.Insert(0, new ListItem("Select Hotel", "0"));
}


public  DataTable MyRstP(SqlCommand cmdSQL)
{
    DataTable rstData = new DataTable();
    using (SqlConnection conn = new SqlConnection(Properties.Settings.Default.TEST4))
    {
        using (cmdSQL)
        {
            cmdSQL.Connection = conn;
            conn.Open();
            rstData.Load(cmdSQL.ExecuteReader());
        }
    }
    return rstData;
}

And then we have the combo box selected index change event - a post back, like any other button click or whatever.

So, we have this code for that combo choice:

protected void DropDownList1_SelectedIndexChanged(object sender, EventArgs e)
{
    if (DropDownList1.SelectedItem.Value != "0")
    {
        // user selected a hotel, get all hotel information
        SqlCommand cmdSQL = 
            new SqlCommand("SELECT * FROM tblHotelsA WHERE ID = @ID");
        cmdSQL.Parameters.Add("@ID", SqlDbType.Int).Value = DropDownList1.SelectedItem.Value;
        DataRow rstData = MyRstP(cmdSQL).Rows[0];

        txtHotel.Text = rstData["HotelName"].ToString();
        tFN.Text = rstData["FirstName"].ToString() ;
        tLN.Text = rstData["LastName"].ToString() ;
        tCity.Text = rstData["City"].ToString() ;
        tProvince.Text = rstData["Province"].ToString();
        chkActive.Checked = (bool)rstData["Active"];
        chkBalcony.Checked = (bool)rstData["Balcony"];
        txtNotes.Text = rstData["Description"].ToString();
    }
}

So, now the result is this:

enter image description here

however, if we did NOT use that !IsPostBack block, then above would not work. But, with this "simple" design pattern?

Then I am free to add more buttons, more code, more events, more post-backs, and they all simple work, and I can ignore the page load event code, since it was only ever used one time on the "real" first page load to load up the data and controls on that page.

Edit2: A grid and edit example

so, lets use all of the above we learned, and build a typical CURD edit example.

So, lets edit a list of hotels, much like above, but we will "pass" our form "div" to some genreal routines I wrote - routines out side of the form.

so, first up, a grid to display the list of hotels (with a edit button).

So, we have this markup:

<asp:GridView ID="GridView1" 
    runat="server" CssClass="table table-hover" AutoGenerateColumns="false"
    width="48%" DataKeyNames="ID" OnRowDataBound="GridView1_RowDataBound"  >
    <Columns>
        <asp:BoundField DataField="FirstName" HeaderText="First Name"  />
        <asp:BoundField DataField="LastName" HeaderText="Last Name"    />
        <asp:BoundField DataField="HotelName" HeaderText="Hotel Name"  ItemStyle-Width="160"  />
        <asp:BoundField DataField="Description" HeaderText="Description" ItemStyle-Width="270" />
        <asp:TemplateField HeaderText="" ItemStyle-HorizontalAlign="Center">
            <ItemTemplate>
                <button runat="server" id="cmdEdit"
                    type="button" class="btn myshadow"
                    onserverclick="cmdEdit_Click">
                    <span class="glyphicon glyphicon-home"></span> Edit
                </button>
            </ItemTemplate>
        </asp:TemplateField>
    </Columns>
</asp:GridView>

Ok, so now our code to load up this grid (with a data table we pull from the database).

We have this (again, that "all important" !IsPostBack stub for our first load of the data.

thus, this:

    protected void Page_Load(object sender, EventArgs e)
    {
        if (!IsPostBack)
            LoadData();
    }

    void LoadData()
    {
        string strSQL = "SELECT * FROM tblHotelsA ORDER BY HotelName";
        DataTable rstData = General.MyRst(strSQL);
        GridView1.DataSource = rstData;
        GridView1.DataBind();
    }

And now we have this:

enter image description here

Ok, so far, not much code (since I have those helper routines in that "general" code class (a static one, since it just a "module" of code I want to call/use/enjoy).

Ok, now in the above, we have a edit button. That edit button will get the current row, pull the data, and then FILL OUT some controls on the page. As noted, you can do assignment of values in code behind, but it really amounts to re-writing the same code over and over. So, after doing that a few times, I wrote some code to do that for me.

So, right below the GV, lets drop in another div. This will be our "edit record" area (to edit details of one row).

So, it really just markup. but, as noted, I had to cook up some kind of "mapping" from the data base columns to the controls on the web page. So, I just out of the blue decided to use a tag "f".

So, f="data base column name" is a standard I adopted.

So, ok, now our markup to edit:

<div id="EditRecord" runat="server" style="float: left; display: none; padding: 15px">
    <div style="float: left" class="iForm">
        <label>HotelName</label>
        <asp:TextBox ID="txtHotel" runat="server" Width="280" f="HotelName" /><br />
        <label>First Name</label>
        <asp:TextBox ID="tFN" runat="server" Width="140"  f="FirstName"/><br />
        <label>Last Name</label>
        <asp:TextBox ID="tLN" runat="server" Width="140" f="LastName" /><br />
        <label>City</label>
        <asp:TextBox ID="tCity" runat="server" Width="140" f="City" /><br />
        <label>Province</label>
        <asp:TextBox ID="tProvince" runat="server" Width="75" f="Province" /><br />
    </div>
    <div style="float: left; margin-left: 20px" class="iForm">
        <label>Description</label>
        <br />
        <asp:TextBox ID="txtNotes" runat="server" Width="400" TextMode="MultiLine"
            Height="150px" f="Description"></asp:TextBox><br />
        <asp:CheckBox ID="chkActive" Text=" Active" runat="server" 
            TextAlign="Right" f="Active" />
        <asp:CheckBox ID="chkBalcony" Text=" Has Balcony" runat="server" 
            TextAlign="Right" f="Balcony"/>
    </div>
    <div style="clear: both"></div>
    <button id="cmdSave" runat="server" class="btn myshadow" type="button"
        onserverclick="cmdSave_Click">
        <span aria-hidden="true" class="glyphicon glyphicon-floppy-saved">Save</span>
    </button>

    <button id="cmdCancel" runat="server" class="btn myshadow" style="margin-left: 15px"
        type="button"
        onclick="MyClose();return false">
        <span aria-hidden="true" class="glyphicon glyphicon-arrow-left">Back/Cancel</span>
    </button>

    <button id="cmdDelete" runat="server" class="btn myshadow" style="margin-left: 15px"
        type="button"
        onserverclick="cmdDelete_ServerClick"
        onclick="if (!confirm('Delete Record?')) {return false};">
        <span aria-hidden="true" class="glyphicon glyphicon-trash">Delete</span>
    </button>

</div>

I mean, it a bit much to post here, but it not a huge amount. it really just amounts to markup I created (and most of it was by drag + drop).

Ok, so now, what does the edit button look like to fill out the above "edit" area?

The edit button code looks like this:

protected void cmdEdit_Click(object sender, EventArgs e)
{
    HtmlButton btn = sender as HtmlButton;
    GridViewRow gRow = btn.NamingContainer as GridViewRow;
    int PKID = (int)GridView1.DataKeys[gRow.RowIndex]["ID"];
    ViewState["PKID"] = PKID;

    string strSQL = $"SELECT * FROM tblHotelsA WHERE ID = {PKID}";
    DataRow rstData = General.MyRst(strSQL).Rows[0];
    General.FLoader(this.EditRecord, rstData);

    // pop the edit div using jQuery.UI dialog
    string sJava = $"pophotel('{btn.ClientID}')";
    Page.ClientScript.RegisterStartupScript(Page.GetType(), "MyJava", sJava, true);
}

So, note the "floader" routine. I passed that "div from the page to the routine that "loads" the database columns into the controls for me. (and I can thus use that one same routine for any web page I create now).

So, next up, we need a "save" button. That code looks like this:

protected void cmdSave_Click(object sender, EventArgs e)
{
    int PKID = (int)ViewState["PKID"];
    General.FWriter(this.EditRecord, PKID, "tblHotelsA");
    LoadData();  // re-load grid to show any changes
}

And while I could have JUST hide the gridview, and display the EditReocrd div with this code:

 GridView1.Style.Add("display", "none");
 EditRecord.Style.Add("display", "nomral");

Well, since I have jQuery, and jQuery.UI as part of this project? I might as well just use a jQuery.UI "dialog" and pop that.

So, out of this whole page, I did have to bite the bullet, and write about 8 lines of js code to pop that dialog.

Not a lot of code, and I have this:

<script>
    function pophotel(btnT) {
        btn = $('#' + btnT)
        var mydiv = $("#EditRecord")
        mydiv.dialog({
            modal: true, appendTo: "form",
            title: "Edit Hotel", closeText: "",
            width: "835px",
            position: { my: 'right top', at: 'right bottom', of: btn },
            dialogClass: "dialogWithDropShadow"
        });
    }
    function MyClose() {
        popdiv = $('#EditRecord')
        popdiv.dialog('close')
    }
</script>

Gee, that is ALL the js code I had to write.

So, now when I run the above, I get this effect:

enter image description here

Now, I can post the "general" routines. I mean, MyRst was simple, and is this:

public static DataTable MyRst(string strSQL)
{
    DataTable rstData = new DataTable();
    using (SqlConnection conn = new SqlConnection(Properties.Settings.Default.TEST4))
    {
        using (SqlCommand cmdSQL = new SqlCommand(strSQL, conn))
        {
            cmdSQL.Connection.Open();
            rstData.Load(cmdSQL.ExecuteReader());
        }
    }
    return rstData;
}

But, as you can see how the simple event model was leveraged, and you can see that we really did not write a lot of code here.

So, I could (am free) to pass any part of that web page class to any other routine I want, so for example, in place of passing the "div" as per my example like this:

 General.FWriter(this.EditRecord, PKID, "tblHotelsA");

I am free to pass the WHOLE page class like this:

 General.FWriter(this.Page, PKID, "tblHotelsA");

And while "General" is a static class? It does/did not have to be for this example.

If YOU HAVE a better web framework that is LESS work then the above?

Then by all means do suggest which one it is!!! - I want to know!!!

Just try the above in MVC, or even introduce say react.js, and you find it takes 3x times the work. So much so, that those frameworks would NEVER post a working example code like I did above, and the reason is simple: it would take WAY WAY too much work and time to do so!!! Webforms very much rock and are amazing as the above example shows, since all of the code was rather simple, and I really did not even require any client side js code, but I tossed in the dialog code for a good example anyway. I REALLY do not know of ANY web based framework that is less efforts for the above given results as compared to how webforms work. Not only are they easy, they also tend to require less code, since we have data bound controls like the gridview I used, and we don't even have any looping code for the grid - we just feed it a datatable.