Capture user input with two-way binding

by Jon Hilton

Sooner or later you're going to want to enable data input for your web app, so users can add, edit, and delete data.

Whether it's to update their profile, complete a short form, or edit rows in a table, inputting data is a major part of the story for many Line Of Business web applications.

So how does WiseJ handle this requirement…

There are a few options, here are the most common (and useful).

Perhaps the simplest is to programmatically retrieve (or set) the value of an input field at the point you need it.

Here's an example:

We have a couple of labels, a TextBox and a Button.

We can wire up an event handler for the button click event, grab the value of the text input, and use it to set a value for our label.

public partial class SimpleInput : Wisej.Web.UserControl
{
    ...

    private void button2_Click(object sender, EventArgs e)
    {
        lblMesage.Text = "Hello " + txtName.Text + "!";
    }
}

This is nice and simple for quickly taking user input and doing something with it but sometimes you'll want a slightly more 'automatic' approach, where you can keep track of values entered by a user.

For this you can use Data Binding.

Simple Binding

You can take a standard input control (like a text box) and bind it to a property on an object.

When the user enters a new value, the object is updated.

public partial class SimpleInput : UserControl
{
    private Person person = new Person();

    public SimpleInput()
    {
        InitializeComponent();
        txtName.DataBindings.Add("Text", person, nameof(person.Name));           
    }

    private void button2_Click(object sender, EventArgs e)
    {
        lblMesage.Text = "Hello " + person.Name + "!";
    }
}

public class Person
{
    public string Name { get; set; }
}

Now the text property of txtName is effectively linked to the Name property on person (an instance of the Person class).

With this we can retrieve the entered value at any time via person.Name, as we do in the button click event handler.

lblMesage.Text = "Hello " + person.Name + "!"; 

But what if we want to keep the UI in sync with changes happening in code.

For example, we might want to wire up a Reset button that clears the person's name when clicked, and therefore clears the textbox.

With the binding in place you might expect you could just reset the value of person.Name and the text input would show the new value (an empty string).

private void btnReset_Click(object sender, EventArgs e)
{
    person.Name = string.Empty;
}

However, as it stands the UI textbox value won't be updated (it will show the previously entered value).

We can solve this by making the underlying Person class implement INotifyPropertyChanged.

INotifyPropertyChanged is a handy mechanism we can use to tell .NET when something has changed.

To implement INotifyPropertyChanged we need to define a PropertyChanged event:

public class Person : INotifyPropertyChanged
{
    public string Name { get; set; }

    public event PropertyChangedEventHandler PropertyChanged;
}

With this in place it's then a question of invoking the PropertyChanged event at the right time (when the Name property's value is updated).

public class Person : INotifyPropertyChanged
{
    private string _name;

    public string Name
    {
        get => _name;
        set
        {
            if (_name != value)
            {
                _name = value;
                PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(Name)));
            }
        }
    }

    public event PropertyChangedEventHandler PropertyChanged;
}

Now every time we set Person.Name to a new value, PropertyChanged will be invoked, and our binding will ensure txtName is updated to reflect the new value.

But what if we want to go a step further and enable editing of multiple records via something like a data grid? For this we may need to turn to a more advanced form of databinding.

Complex Binding

Say, for example, you want to present a number of records in a grid (as we saw when we explored showing data via WiseJ) and enable editing of those records.

One option here is to use a BindingList.

This is particularly handy when you have any kind of list of objects and want to hook them up to a data control (such as a DataGridView).

For example, we can create a Binding List of Person objects:

public partial class ComplexInput : Wisej.Web.UserControl
{
    BindingList<Person> people = new BindingList<Person>();

    public ComplexInput()
    {
        InitializeComponent();        

        dataGridView1.AllowUserToAddRows = true;
        dataGridView1.AllowUserToDeleteRows = true;
        dataGridView1.DataSource = people;            
    }

}

I've added a DataGridView (dataGridView1) via the WYSIWYG editor then hooked that up to the BindingList via the page's constructor.

With this we get a list of people's names, and can add, edit and delete records safe in the knowledge that the underlying list will be updated accordingly.

This is all well and good for in memory lists, but what if we want to persist changes to a database and perhaps provide our own custom UI for editing?

Putting it all together with a real database

Let's look at a concrete example where we'll use a combination of the DataGridView to show a list of movies, and a little bit of Simple Binding to present a custom edit dialog for editing a movie's title.

In the previous article we used Dapper to fetch a list of movies from a database and present it to the user via a DataGridView.

DapperExample.cs

private async void LoadData()
{
    using (var connection = new SQLiteConnection(DBConfig.ConnectionString))
    {
        this.moviesBindingSource.DataSource = await connection
            .QueryAsync<MovieSummary>("SELECT Title, Summary FROM Movie");

        this.dgMovies.DataSource = moviesBindingSource;              
    }
} 

Now let's suppose we want to enable users to edit the title of any of the movies in this list.

In this case we want to show a custom dialog window which can be used to edit the title.

Here's the flow we're aiming for:

  • User double-clicks the row for the movie they want to edit

  • A dialog window opens with a form containing a single text input for the movie's title

  • The user makes their changes and clicks Save (or OK)

  • The new values are used to update the relevant record in the database

We can use the grid view's DoubleClick event to wire up the DoubleClick event:

DapperExample.cs

+...

private async void dgMovies_CellDoubleClick(object sender, Wisej.Web.DataGridViewCellEventArgs e)
{
    if(e.RowIndex >= 0)
    {
        using (var connection = new SQLiteConnection(DBConfig.ConnectionString))
        {
            // figure out which item was selected in the grid
            MovieSummary selectedItem = (MovieSummary)dgMovies.Rows[e.RowIndex].DataBoundItem;
                        
            var selectQuery = "SELECT * FROM Movie where Id = @id";
            
            // fetch the details for the specific movie
            var details = await connection.QuerySingleOrDefaultAsync<MovieDetails>(
                selectQuery, new { selectedItem.Id });

            // create a new instance of the dialog window (and pass along the movie details via its constructor)
            using (EditMovieDialog editDialog = new EditMovieDialog(details))
            {
                // show the dialog
                var result = editDialog.ShowDialog();
                
                // run this logic when the dialog result is 'OK'
                if(result == DialogResult.OK)
                {
                  // save changes
                  // refetch data
                }
            }
        }
       
    }
}

We start by locating the currently selected row in the grid view:

MovieSummary selectedItem 
    = (MovieSummary)dgMovies.Rows[e.RowIndex].DataBoundItem;                

With this we know the Id of the movie we want to edit.

We can then go ahead and use Dapper to fetch the relevant details.

var selectQuery = "SELECT * FROM Movie where Id = @id";
 
 // fetch the details for the specific movie
 var details = await connection.QuerySingleOrDefaultAsync<MovieDetails>(
     selectQuery, new { selectedItem.Id });

Once we have the details we can instantiate then show our editing window (theEditMovieDialog dialog), giving it the movie details we just fetched from the database.

using (EditMovieDialog editDialog = new EditMovieDialog(details))
{
    // show the dialog
    var result = editDialog.ShowDialog();
    
    ...
}

Here's the dialog itself:

EditMovieDialog

public partial class EditMovieDialog : Form
{
    public Data.MovieDetails UpdatedDetails { get; set; }

    public EditMovieDialog(Data.MovieDetails details)
    {
        InitializeComponent();

        this.UpdatedDetails = details;
        txtTitle.DataBindings.Add("Text", UpdatedDetails, nameof(UpdatedDetails.Title));           
    }

    private void btnSave_Click(object sender, EventArgs e)
    {
        this.DialogResult = DialogResult.OK;
        this.Close();
    }
}

We use bindings (as we saw earlier) to ensure the value entered into the txtTitle text input is reflected in the underlying model (UpdatedDetails).

When the user clicks the OK button the dialog result is set to OK and the dialog is closed.

Back in our main page we can retrieve the updated details, make a call to update the DB (and then re-fetch the data so we can show it in our grid view).

...

using (EditMovieDialog editDialog = new EditMovieDialog(details))
{
    var result = editDialog.ShowDialog();
    if(result == DialogResult.OK)
    {
        var updatedRecord = editDialog.UpdatedDetails;

        var dbSaveResult = connection
            .Execute("UPDATE Movie set Title = @title where Id = @id", 
            new { editDialog.UpdatedDetails .Title, selectedItem.Id});

        LoadData();
    }
}

With this we have a list of movies with a handy option for the user to edit each movie's title.

When the user clicks Save the dialog is closed (with an OK dialog result) and the code in the main DapperExample page calls the database to update the selected movie's title.

In Summary

There are numerous methods for accepting user input via WiseJ, here we've explored some of the key underlying mechanisms.

You can interact with input controls (like a text box) directly or use binding to ensure you always have access to their current values.

INotifyPropertyChanged is useful to ensure your UI stays in sync with your models.

Finally, complex binding is useful when you want to interact with records via built-in data presentation controls like a grid view.

Last updated

Logo

© Ice Tea Group LLC, All Rights Reserved.