best practices: I'm doing it wrong



  • Hi all -

    So, as many of you are aware, I've been working on an app that will allow the user to look at and modify information about various devices on a network. My original plan was to have a list of the devices at the top of the UI, and underneath the summary would be a bunch (~15) of details about the device. The user would select a device from the list, and the details for that device would update.

    As mentioned, the app is to allow the user to modify this data as well as just read it. My original plan was to permit in situ modification, and a "commit" button. I now realize that this may not be the best way to go. At a minimum, I'll need to disable updates to the details while the user is editing. There's also an issue of determining when the user wishes to edit rather than just view. These are solvable but have me questioning my original design.

    So, since many of you have been around this tree, what is best practice for a UI displaying information that may be updated from multiple sources? Would it be better if I replaced the "commit" button with an "edit" button, which would open a new window displaying the details to be edited? These fields wouldn't be modified by the program, only from user input.

    Any other suggestions are most welcome.



  • @mzimmers
    It's an eternal question whether editing of master-detail is best doing directly in the detail view or in a separate dialog, and whether to allow editing immediately or to require "start-edit" & "edit-edit"....

    At a minimum, I'll need to disable updates to the details while the user is editing.

    What's this about?



  • @JonB what I meant was that it would be a very poor UX to have details changing automatically at the same time the user is trying to change them himself. If I'm going to enable in place editing, it seems almost axiomatic that I have to disable remote updates while the edit session is active. Not really a big deal, but something I'll have to be mindful of, and there would have to be some determination of when the edit session has begun. Would it be when the user selects an item from the summary list, or when he first focuses on an editable field, or something else?


  • Lifetime Qt Champion

    Hi,

    Are the details you show all refreshed from the device ? Or are some of them parameters and others values that are just read ?



  • @SGaist Most but not all (I'd say about 13 out of 15) will be reported from the remote devices.



  • @mzimmers
    And you're saying these are being updated constantly asynchronously, I didn't know that.

    Something seems a bit strange: what fields is the user editing, versus what is being fetched from the devices? Is the user really editing those 13 values which are reported? You might be in danger of allowing user to set a value at the start of the editing snapshot to be sent to the device on update, when the device has been reporting it changing during editing?? Are the columns allowing editing 100% distinct from the columns being sent from the device?



  • Upon reflection, I think the answer I gave to SGaist, while correct, was a little misleading. Each device on the network will occasionally broadcast a "heartbeat" message to notify the host that he's still alive, and to convey status information. Here's a struct containing the various items to be contained in the report:

    struct DeviceDetails
    {
        uint8_t macAddr[6];
        char serialNbr[9];
        char devName[25];
        uint8_t version[4];
        uint8_t ssid[64];
        uint8_t psk[64];
        uint8_t ipConfig; // 0 = DHCP, 1 = static.
        char ipAddr[16];
        char gateway[16];
        char subnet[16];
        char ntpServer[256];
        char timeZone[32];
        uint8_t ledPattern;
        uint8_t ledBrightness;
    };
    

    After thinking about this a little more, I now realize that the device will almost never make autonomous changes to this data. So...would this imply that it's reasonable to allow the user to edit in the main details area, with a "commit" button and a confirmation dialog?

    EDIT: perhaps a pic would help. This is my main window in a partial state of completion.
    0_1530111647683_wifibutton.PNG
    The area below the title is a QTableView which lists the various devices on the network, and allows the user to select one by clicking on its row. The details fields are below the line.



  • @mzimmers
    So I can edit the 5 fields I see in your widgets, but not the other columns, when I'm in your edit mode?



  • @JonB eventually all the columns would be editable; I just haven't bothered to implement them yet. I wanted to make sure I was doing this correctly before I did the others.



  • And, a related question: when the user presses the "commit" button, what should happen?

    I'm thinking the widget sends a signal to the worker object, informing him to send a change message to the target device. But the worker needs a copy of the changed details. Should I manually construct a copy of them from the values mapped to my display, like this:

            Message msg;
            string s;
            s = ui->lineEditMacAddr->text().toLocal8Bit().toStdString();
            msg.add(msgTags[TAG_MACADDRESS], s);
            ...// repeat above line for each detail
            emit editCommitted(msg);
    

    I can certainly do this, but I was wondering whether there was a preferable method.



  • @mzimmers
    Well yes it is common to gather the values out of your individual input widgets, copy them out into an abstracted structure shared by the front- & back-ends, and send it some "update" signal, like your sample code. Certainly you do not want to pass the widgets themselves to the back-end.



  • Excellent...that gives me something to work on this afternoon. Thanks, JonB.



  • I've decided/realized that I don't want the details portion of the UI to be updated whenever the program receives a message. Because of this, it seems that I shouldn't map the details area to the model, but simply update the fields (manually) when a row is selected from the summary table.

    The user then edits the fields as he wishes, and presses "commit" which gathers up all the information in the details area, and sends them to the worker.

    Does this seem like a valid approach?



  • Just now returning to this issue.

    So, my UI has a summary area which receives contstant remote updating. The user selects an item from the summary field, and details about that item are displayed in another area. The goal is to allow the user to view AND UPDATE fields in this details area.

    If the details are mapped to the model, they're subject to being overwritten by remote updates. This can disrupt a user's editing session. I imagine this is a classic UI dilemma; how is it typically resolved?

    Thanks...



  • @mzimmers
    Well at some level I think you have to decide whether/when to start "editing" by the user and pause "updating" from the device. Conceptually first. You don't really want the user to start changing fields if they're still liable to get auto-updated while he's doing so.

    That probably means you need a "start edit", a bit like a database BEGIN TRANSACTION. This seems to naturally be your "user selects a row [and starts editing]". Though it's not so clear whether it should be as soon as the row is selected or only once the first edit is actually started (there could be a lot of time between those two actions). Then your "commit" button is like a database COMMIT TRANSACTION, or clicking to another row instead abandons the edit and is like a ROLLBACK TRANSACTION.

    But there are still many ways you have to decide for committing the changes. Do you update all columns or only certain ones? Do you ignore/overwrite any changes which may have happened from the device while the editing has been going on, or do you want actually to abort the update if user has changed a field which the device has also changed? These are similar to database issues, like pessimistic and optimistic locking strategies.

    I think: think out what you actually want for the behaviour conceptually, only then decide which way to code to suit that.


  • Qt Champions 2017

    @mzimmers said in best practices: I'm doing it wrong:

    If the details are mapped to the model, they're subject to being overwritten by remote updates. This can disrupt a user's editing session. I imagine this is a classic UI dilemma; how is it typically resolved?

    If it were me I'd keep the updating part non-editable. I'd rather just open a dialog with the data loaded at the time of the edit trigger being fired and then leave the user either to commit or discard his changes. The other thing just seems to confusing to me as a user.



  • @JonB
    I agree with @kshegunov that keeping the auto-updating and editing parts separate would simplify the experience for the end user.

    However, using a dialog and batch-updating will still leave you with decisions about what to really update, e.g.:

    • A widget has an initial value from the device, copied into the dialog.
    • The end-user does not change that, but changes some other widget's value, and clicks "commit".
    • Meanwhile, the first widget's underlying value on the device has changed.

    What do you do? Do you commit the value currently seen by the user in the dialog back to the device thus changing it on the device, or do you not do so, thus preserving the new value in the device but not what the user saw on the dialog? And how do you recognise this situation one way or the other?

    An alternative approach which avoids all of this: Do you need/want to batch up multiple changes? Are they inter-dependent or quite separate? Could you instead send a change to one widget's value back to the device immediately (and perhaps even get rid of the dialog completely and/or no need for the "abstraction" layer)? Your code probably actually becomes a lot simpler. It could be rather nice for the user to see his changes applied immediately as he makes them, no "commit" button?



  • Here's an idea, for what I would do.

    When the user selects an item from the summary field the details about that item are captured in a "snapshot" as of the moment of the click.

    The user than can modify that captured data how ever allowed, and he/she can submit it via a special button.

    However, while the user is editing, you keep on monitoring the data and if the new data varies from the original captured one, you blend in a small indicator item. For example a red ! next to the edit field, to let the user now, that this entry is out of date. The use can than choose to update that field by clicking on the Icon.

    That updates the field and updates to captured data as well, reading it for the next potential change.



  • Lots of good information in these replies; thanks, guys. Jon: the information in question isn't going to be updated frequently (in fact, most of this app's use will be for initial provisioning), but any committed changes will involve a write to NVS (non volatile storage) on the target device. Since this is (relatively) slow, and because NVS has a limited number of writes, it's probably best to avoid on-the-fly commits, and let the user perform a manual commit instead.

    Based on what all of you have said, it does seem like a separate window is probably the right choice.



  • I gather that a QDialog is the right UI object to use for this. I'd like to use Designer to create the dialog, so what is the mechanism for attaching it to my main display widget? Do I use an analog to the main widget:

    namespace Ui {
    class Widget;
    }
    
    class Widget : public QWidget
    {
        Q_OBJECT
    
    private:
        Ui::Widget *ui;
    

    or is there a preferred method? One of the examples drew heavily on layouts and addWidget() calls, but this was for a considerably more elaborate dialog than I anticipate needing.

    Thanks...


  • Qt Champions 2017

    hi
    Its 100% like a QWidget except

    class MyDialog: public QDialog

    I made a sample for some other poster about sending signals to mainwindow from dialog
    But i guess it also shows how to use dialogs.
    https://www.dropbox.com/s/w1qo7xpjhhxjzhi/myotherdialog.zip?dl=0

    note, there is a difference.
    if you show it using exec() it blocks access to mainwindow while open.
    if you show it with show() it wont.
    That is different from QWidget windows as they cant block that way.



  • @mrjj thanks for the example. It appears that I was on the right track.

    I'm trying to use your example as a model, but I'm getting a compiler error. Here's the header file:

    #ifndef EDITDIALOG_H
    #define EDITDIALOG_H
    
    #include <QDialog>
    
    namespace Ui {
    class EditDialog;
    }
    
    class EditDialog : public QDialog
    {
        Q_OBJECT
    
    public:
        explicit EditDialog(QWidget *parent = nullptr);
    
    private:
        Ui::EditDialog *ui;
    
    signals:
    
    public slots:
    };
    
    #endif // EDITDIALOG_H
    

    and here's the source:

    #include "editdialog.h"
    
    EditDialog::EditDialog(QWidget *parent) : QDialog(parent), ui(new Ui::EditDialog)
    {
    }
    

    But I'm getting a compiler error about an incomplete type Ui::EditDialog. I can't see what I'm missing; does anything look wrong to you?

    Thanks...


  • Qt Champions 2017

    @mzimmers
    Did you make it with UI ?
    You seems to be missing
    #include "ui_EditDialog .h"



  • I've never needed a line like #include "ui_EditDialog .h" before. But I did try including it, and I get the same error message.

    Not sure what yopu mean by "make it with UI" though...


  • Qt Champions 2017

    @mzimmers
    when you use the wizard to make a new class, you can select
    alt text
    and then base class
    alt text

    Then the besides .h and .cpp u have .ui file which you can use for visual designing.
    that also adds the Ui::EditDialog *ui; to the class def.

    if you dont want it, you can simply remove all of it.
    and create widgets in code.



  • I'm doing something wrong. I deleted all my editdialog files, and recreated the .ui file. I then added the class, but Creator doesn't add the UI::EditDialog *ui for me.
    EDIT: I think I found the problem -- used Qt Designer Form instead of Qt Designer Form Class. Will report back momentarily.


  • Qt Champions 2017

    @mzimmers
    Yep the Designer form is without .h and cpp.
    Use the Class version to get complete kit.


  • Qt Champions 2017

    The name of the class follows what you give it for the root object in the form editor - right pane, topmost entry.



  • Based on input from a few people here, I've decided to leave the details in the main display disabled. The user will press an "edit" button which will create a QDialog with the details enabled for editing.

    It's not a huge deal, but I will be performing redundant work if I just re-create the display elements for the details in the QDialog. Does it make sense to create an aggregate display element with all the details, and use this element in both the main and edit windows? Can .ui files be nested?



  • I've created an edit dialog, but I'm not able to map the individual elements to the model as I did in the main widget. I create the edit dialog like this:

    DeviceModel *m_d;
    ...
    void Widget::on_pushButtonEdit_clicked()
    {
        EditDialog editDialog(m_d);
        QObject::connect(&editDialog, &EditDialog::editCommitPressed, this, &Widget::sendCommit);
        editDialog.exec();
    }
    

    And I try to map to the model like this (seemingly identical to how I do it in the main widget):

    EditDialog::EditDialog(DeviceModel *d, QWidget *parent) :
        QDialog(parent),
        ui(new Ui::EditDialog)
    {
        ui->setupUi(this);
        m_mapper = new QDataWidgetMapper(this);
        m_mapper->setModel(d->getModel());
    
        m_mapper->addMapping(ui->deviceName, TAG_DEVICENAME);
    

    The mapping works in the main widget, but not for my edit dialog. Any ideas what I'm doing wrong? Thanks.



  • Did you forget to connect to setCurrentModelIndex?



  • Well...maybe.

    In my main widget, I have this line:

        QObject::connect(ui->tableView->selectionModel(), &QItemSelectionModel::currentRowChanged,
                         m_mapper, &QDataWidgetMapper::setCurrentModelIndex);
    

    Wouldn't that be sufficient for anything using the same model?



  • @mzimmers said in best practices: I'm doing it wrong:

    for anything using the same model

    The connect uses the selection of the view and the mapper so has nothing to do with the model.
    If either the view or the m_mapper are different objects from those in the main window then you have to redo the connection



  • Oh, OK, I think I see the problem: since my edit dialog doesn't have a QTableView, there's no way to select a row (no table --> no rows). I guess I can't just have the details mapped without some table to give it context, right?

    So, do I need an invisible table in my edit dialog, or is there some more clever way around this?



  • No, you can also just straight call m_mapper->setCurrentModelIndex(index); where index is the index of the model you want to load without doing any connection



  • I'm getting a bad index back. Here's my c'tor:

    EditDialog::EditDialog(DeviceModel *d, QWidget *parent) :
        QDialog(parent),
        ui(new Ui::EditDialog)
    {
        ui->setupUi(this);
        m_mapper = new QDataWidgetMapper(this);
        m_mapper->setModel(d->getModel());
    
        int i = m_mapper->currentIndex();
        QModelIndex qmi = d->getModel()->index(i, 0);
        m_mapper->setCurrentModelIndex(qmi);
    

    The variable i is set to (-1). What might I be doing wrong?



  • create a public method in the dialog:
    void EditDialog::loadIndex(const QModelIndex& idx){m_mapper->setCurrentModelIndex(idx);}

    After EditDialog editDialog(m_d);, add
    editDialog.loadIndex(ui->tableView->selectionModel()->currentIndex());



  • @VRonin a couple questions, please:

    1. EditDialog doesn't have a method setCurrentModelIndex(). Perhaps you meant:
    QDataWidgetMapper *m_mapper;
    ...
    m_mapper.setCurrentModelIndex(ui->tableView->selectionModel()->currentIndex());
    
    1. How does loadIndex() get invoked?

    Thanks...



  • Edited the answer above. editDialog.setCurrentModelIndex should have been editDialog.loadIndex sorry



  • Clever...very clever. So, to summarize:

    1. it's generally a good idea to perform editing in a separate window/dialog.
    2. create this window with new->Qt Designer Form Class; this create everything at once.
    3. use the QDataWidgetMapper in the edit dialog to display the details from the model.
    4. use VRonin's technique of setting the model index for the edit dialog by using the index from the parent window.

    I'll edit the summary if anyone informs me of errata. Thanks for all the assistance.


Log in to reply
 

Looks like your connection to Qt Forum was lost, please wait while we try to reconnect.