Filling In Fields For The User

Say, we have some textboxes we can use to edit data about a person. Some data we already have stored in a database. All we do is fill in the passport number, press 'Find' and wait for the system to fill in the remaining fields. Once the fields are filled, we do some editing and save the changes. Common scenario, isn't it?

Only it may not be that simple after all. From a developer's point of view the task includes storing data in a view model, making sure the user controls are disabled during the data loading process, enabling the controls again when all the fields are populated, storing the latest version of the edited object which would always be consistent. The more the requirements the uglier the solutions that spring to mind.

Again, let's look at some inspiring picture... like the one below.



What's so inspiring about it? Well, the picture is about switching between data sources and clearly seeing the current choice. It was itself inspired by the idea of a flight control system.

When flying an aircraft the pilot may choose between steering it manually or using an autopilot. Disengage the autopilot, you can steer the plane by hand using cockpit controls. Engage the autopilot, it will fly the plane by sending directions directly to the on-board computer and keep the crew in the know of what is happening by visually moving the controls (e.g. turning the yoke).
  • c - a cockpit control,
  • AP Off / On Switch - an autopilot off/on switch,
  • AP input - autopilot input,
  • FCC - flight control computer,
  • Input - directions to FCC,
  • Output - commands to actuators.
There are peculiarities to flight control systems, however, even the core idea of their functioning is useful to us. It even translates into our notions.
  • c - a textbox,
  • AP Off / On Switch - a holding switch (see Start/Stop pattern),
  • AP input - an employee profile read from the database,
  • FCC - a property that gets its value from the source defined by the switch,
  • Input - an employee profile received by the output property,
  • Output - current state of the edited employee profile (output property).
Let's illustrate the load-by-wire pattern by implementing a solution to the original problem in code. We won't mention the saving logic (which would only add a couple of lines to our code) in this example. An extended version of the solution (with validation) you can find in source Code under the project Property.Demo.Wpf.

public class MainWindowViewModel
{
    public MainWindowViewModel()
    {

        //=== User input ===// 

        var throttle = TimeSpan.FromMilliseconds(200);

        // Inputs
        _PassportNr = RxProperty.ThrottleInput<string>(throttle).Default("")
            .TriggersReload(_ => _Employee)
            .TriggersReloadIf((_, o) => o != _Employee.Value.PassportNr);

        _FirstName = RxProperty.ThrottleInput<string>(throttle).Default("")
            .TriggersReload(_ => _Employee)
            .TriggersReloadIf((_, o) => o != _Employee.Value.FirstName);

        _LastName = RxProperty.ThrottleInput<string>(throttle).Default("")
            .TriggersReload(_ => _Employee)
            .TriggersReloadIf((_, o) => o != _Employee.Value.LastName);

        _Company = RxProperty.ThrottleInput<string>(throttle).Default("")
            .TriggersReload(_ => _Employee)
            .TriggersReloadIf((_, o) => o != _Employee.Value.Company);

        _Email = RxProperty.ThrottleInput<string>(throttle).Default("")
            .TriggersReload(_ => _Employee)
            .TriggersReloadIf((_, o) => o != _Employee.Value.Email);

        // Aggregated user input
        _InputEmployee = SyncProperty.SyncGet<Employee>(o =>
        {
            if (_Employee.Value.PassportNr != _PassportNr.Value)
                // New record mode
                return new Employee(-1, 
                    _PassportNr.Value,
                    _FirstName.Value,
                    _LastName.Value,
                    _Company.Value,
                    _Email.Value);
            else
                // Edit mode
                return new Employee(_Employee.Value.Id,
                    _PassportNr.Value,  
                    _FirstName.Value,
                    _LastName.Value,
                    _Company.Value,
                    _Email.Value);
        });

        //=== DB input ===// 

        // Db data loading starter
        _DbChain = new ChainStarter(_ => _DbEmployee,      // Start loading db record
                                    _ => _DbInputStatus,   // Status -> Loading
                                    _ => _InputPermitted); // User input -> disable

        // DB data record
        _DbEmployee = RxProperty.AsyncCommand<Employee>(o => 
                  EmployeeService.GetEmployee(_PassportNr.Value))
            .TriggersReload(_ => _Employee,        // Send to out property
                            _ => _DbChain.Stop,    // Signal 'Loading finished'
                            _ => _InputPermitted); // User input -> enable

        //=== System status ===// 

        // DB data loading status 
        _DbInputStatus = SyncProperty.SyncCommand<LoadingStatus>(o =>
        {
            if (_DbEmployee.IsLoading) return Wpf.LoadingStatus.InProcess;
            if (!_DbChain.IsRunning.Value) return Wpf.LoadingStatus.NoActivity;
            return _DbEmployee.Value != null ?
                Wpf.LoadingStatus.RecordsFound
                : Wpf.LoadingStatus.EmptyResultSet;
        });

        // User input On/Off switch
        _InputPermitted = SyncProperty.SyncCommand<bool>(o =>
            !_DbChain.IsRunning.Value)
            .Default(true);

        //=== Output ===// 

        // Output property
        _Employee = SyncProperty.SyncCommand<Employee>(o =>
        {
            if (_DbChain.IsRunning.Value)
                return _DbEmployee.Value ?? _InputEmployee.Value;
            else
                return _InputEmployee.Value;
        })
        .Default(new Employee(-1, "", "", "", "", ""))
        .TriggersReload(_ => _DbInputStatus, // Update db input status
                        _ => _Feedback);     // Send to feedback

        //=== Feedback ===// 

        // Records found -> send back to controls
        _Feedback = SyncProperty.ActionEndPoint(() =>
        {
            if (_DbInputStatus.Value != Wpf.LoadingStatus.RecordsFound) return;
            FirstName.InputValue = Employee.Value.FirstName;
            LastName.InputValue = Employee.Value.LastName;
            Company.InputValue = Employee.Value.Company;
            Email.InputValue = Employee.Value.Email;
        });
    }

    //=== Declaration ===// 

    private readonly IInputProperty<string> _PassportNr;
    public IInputProperty<string> PassportNr { get { return _PassportNr; } }

    private readonly IInputProperty<string> _FirstName;
    public IInputProperty<string> FirstName { get { return _FirstName; } }

    private readonly IInputProperty<string> _LastName;
    public IInputProperty<string> LastName { get { return _LastName; } }

    private readonly IInputProperty<string> _Company;
    public IInputProperty<string> Company { get { return _Company; } }

    private readonly IInputProperty<string> _Email;
    public IInputProperty<string> Email { get { return _Email; } }

    private readonly ISyncProperty<Employee> _InputEmployee;

    //=//

    private readonly ChainStarter _DbChain;
    public ChainStarter DbChain { get { return _DbChain; } }

    private readonly IAsyncCommandProperty<Employee> _DbEmployee;

    //=//

    private readonly ISyncCommandProperty<LoadingStatus> _DbInputStatus;
    public ISyncCommandProperty<LoadingStatus> DbInputStatus 
                                                { get { return _DbInputStatus; } }

    private readonly ISyncCommandProperty<bool> _InputPermitted;
    public ISyncCommandProperty<bool> InputPermitted { get { return _InputPermitted; } }

    //=//

    private readonly ISyncCommandProperty<Employee> _Employee;
    public ISyncCommandProperty<Employee> Employee { get { return _Employee; } }

    //=//

    private readonly IEndPoint _Feedback;
}
The above solution is implemented in such a way that no matter which properties are used on the outside, our logic is safe and stable, all kept in one place and it's cohesive. It works just like the picture suggests except it uses a db input status to break the feedback circuit if no data have been read from the database (not only if 'user input' mode is active). All user input signals are aggregated in a single immutable object. We get the latest version of it by using a Get Property (a good example of how this property pattern may be used).

Here is the XAML part of the solution.

<TextBox Name="PassportNrTextBox" 
            IsEnabled="{Binding InputPermitted.Value}"
            Text="{Binding PassportNr.InputValue, UpdateSourceTrigger=PropertyChanged}"/>
<TextBox Name="FirstNameTextBox" 
            IsEnabled="{Binding InputPermitted.Value}"
            Text="{Binding FirstName.InputValue, UpdateSourceTrigger=PropertyChanged}"/>
<TextBox Name="LastNameTextBox" 
            IsEnabled="{Binding InputPermitted.Value}"
            Text="{Binding LastName.InputValue, UpdateSourceTrigger=PropertyChanged}"/>
<TextBox Name="CompanyTextBox"
            IsEnabled="{Binding InputPermitted.Value}"
            Text="{Binding Company.InputValue, UpdateSourceTrigger=PropertyChanged}"/>
<TextBox Name="EmailTextBox" 
            IsEnabled="{Binding InputPermitted.Value}"
            Text="{Binding Email.InputValue, UpdateSourceTrigger=PropertyChanged}"/>
<Button Content="Find"
             IsEnabled="{Binding InputPermitted.Value}"
             Command="{Binding DbChain.Start.ReloadCommand}"/>
<ContentPresenter Content="{Binding DbInputStatus.Value, Converter={StaticResource LoadingStatusConverter}}"/>
Below you can also see the converter used to present the db input status in the form of a readable string.

public class LoadingStatusConverter : IValueConverter
{
    public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
    {
        var status = (LoadingStatus)value;

        switch (status)
        {
            case LoadingStatus.NoActivity:
                return "";
            case LoadingStatus.InProcess:
                return "Loading...";
            case LoadingStatus.EmptyResultSet:
                return "No employee data records found.";
            case LoadingStatus.RecordsFound:
                return "1 employee data record(s) found.";
            default:
                return "";
        }
    }

    public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
    {
        throw new NotImplementedException();
    }
}
The above solution have been designed to easily adapt to any new requirements giving us tools (statuses and input/output values) that are easy and safe to use. That's why the above load-by-wire pattern dramatically reduces the chance that your system will ever crash.


Last edited Jul 2, 2013 at 6:18 PM by Vitaly_Kamiansky, version 15

Comments

No comments yet.