2007-07-31

Dude, where's my DataItems?

ASP.Net coding problem of the day: I have a List of objects, and I have a web page that needs to display them all, allow additions and edits, and save all changes in a batch. Each object has just over a dozen properties, some of which are dependent on others. Also, this is code I inherited from a developer who has since moved on to other things.

What was coded so far was fairly straightforward. There was a user control created with all the TextBoxes and DropDowns, and a page that contained a Repeater control with the user control in it (plus a header and an AJAX CollapsiblePanelExtender to make it look nice and be usable). Most of the code was in place to retrieve the values for the DropDowns, retrieve the object List, and bind the object List to the Repeater. The only thing missing was the actual binding of an object to the user control.

I researched databinding syntax a bit and discovered that the Bind function allows for a two-way binding. I used that in the user control, but I discovered a small problem when trying to bind in the Repeater. Bind attempts to bind a property on the current DataItem, but in the Repeater, what I wanted to do was bind the DataItem (the object) itself to the control. I'm not sure, if I were able to accomplish this, if it would have solved my subsequent problems, but since it wasn't possible, I suppose that question is moot. My only choice was to, in the Repeater, set a property on the user control to <%# Container.DataItem %>.

As I was working with the user control, I found it advantageous to override the SaveViewState and LoadViewState methods, so that each user control saved its associated object in ViewState. (This web app is designed for use on an intranet, so I could afford the extra data transfer.) This would turn out to be my salvation later.

I ran into my first problem when I tried to implement the "Add" button on the page. The Repeater's DataSource is not automatically saved for PostBack, so attempting to add a new object to the List didn't work (the DataSource, and therefore the List, didn't exist). The simple solution was to save the List in the page's ViewState, and in Add_Click, get the List, Add a new object, and re-bind it to the Repeater. The problem was, when I changed some data in the TextBoxes and DropDowns of existing controls, clicking the Add button reverted all values back to their originals.

After much searching (finding many people running into issues with the non-persistence of DataSources and RepeaterItem DataItems), overriding various page- and control-level events, I finally worked out the answer.

The alleged two-way binding doesn't work in this case. Fortunately, the previous developer had already written (most of) a method to populate an object with the values from the on-screen controls, because I would need it. I found that, on PostBack, the Repeater control still contains the controls from the last page load, even if the DataItem itself is lost. So, what I did in the page's Load event, was to have it reconstruct the List based on the controls' copies of the objects (which, remember, were saving them in their own ViewStates). Fortunately, at this stage in the lifecycle, the controls' TextBoxes and DropDowns had already been updated with the changes typed in, so in the control's get accessor for the bound property, I had the control update its object with the values from the TextBoxes before returning it.

In short, the key parts of the code look like this:

MyUserControl.ascx.cs

private MyObject _myObject;

protected override object SaveViewState() {
    object[] vs = new object[] { _myObject, base.SaveViewState() };
    return vs;
}
protected override void LoadViewState(object savedState) {
    object vs;
    if (savedState is object[] && ((object[])savedState).Length == 2) {
        object[] myVS = (object[])savedState;
        _myObject = (MyObject)(myVS[0]);
        vs = myVS[1];
    } else {
        vs = savedState;
    }
    base.LoadViewState(vs);
}
public MyObject MyObject {
    get {
        SaveControlsToObject(_myObject);
        return _myObject;
    }
    set {
        if (_myObject == null) _myObject = value;
    }
}

WebPage.aspx

<asp:Repeater ID='repeater1' runat='server'>
    <ItemTemplate>
        <uc:MyUserControl ID='userControl1' runat='server' MyObject='<%# Container.DataItem %>' />
    </ItemTemplate>
</asp:Repeater>

WebPage.aspx.cs

protected void Page_Load(object sender, EventArgs e)
    if (!IsPostBack) {
        repeater1.DataSource = GetMyObjectList();
        repeater1.DataBind();
    } else {
        //rebuild the data source from the controls
        List<MyObject> myObjects = new List<MyObject>();
        foreach (RepeaterItem repeaterItem in repeater1.Items) {
            MyUserControl myControl = (MyUserControl)repeaterItem.FindControl("userControl1");
            myObjects.Add(myControl.MyObject);
        }
        repeater1.DataSource = myObjects;
    }
}
protected void Add_Click(object sender, EventArgs e) {
    MyObject newObject = new MyObject();
    newObject.someProperty = "initial value";
    ((List<MyObject>)repeater1.DataSource).Add(newObject);
    repeater1.DataBind();
}

Implementation of the Save function is left as an exercise for the user. ;)

2007-07-25

To all legitimate MX admins: Please name your mail servers

I realize my blog doesn't get a whole lot of traffic, but I would like to post this plea to anyone who administers a legitimate mail server that sends email.

Spam sucks. Those of us who run email servers are involved in a constant struggle against the forces of evil. Personally, I run a postfix email server, and I have enabled a series of checks to validate incoming requests. Since turning these on, the signal-to-noise ratio of incoming email is quite high. One of these checks is hostname lookup. My server, when it receives a request from some machine, must be able to find the name of that machine before it will accept email. A failure appears in the mail.log like so (I get a few dozen or so of these per day):

postfix/smtpd[9739]: NOQUEUE: reject: RCPT from unknown[www.xxx.yyy.zzz]: 450 Client host rejected: cannot find your hostname, [www.xxx.yyy.zzz]; from=<spammer@bogusdomain.nul> to=<someaddress@mydomain.nul> proto=ESMTP helo=<another.fakedomain.nul>

Recently, though, it seems there have been some community sites in which I have been interested, but when I try to sign up for them, I can't get the confirmation email (which is often the key to participation). If it was just one or two, I could accept it and move on, but there have been several in recent memory. When I check the mail.log, I can find these attempts with the above error.

While I could turn off this spam trap, or add exceptions for every site from which I want email, I find this unacceptable. There should be no reason for having to go through a configuration change to open myself to spamming, or make individual config changes for every site -- especially when this can be avoided by administrators being responsible for proper configurations. There's a reason this rule exists built-in to my mail server -- because it's a good practice to follow, and it's a practice that spammers are likely to break.

So, if you run a mail server, please make sure your outgoing email server identifies itself appropriately. It's for the common good, after all.