<?xml version="1.0" encoding="utf-8" standalone="yes"?><rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom"><channel><title>Model-View on Roberto Selbach</title><link>https://rselbach.com/tags/model-view/</link><description>Recent content in Model-View on Roberto Selbach</description><generator>Hugo</generator><language>en-US</language><lastBuildDate>Fri, 06 Jun 2008 18:35:00 +0000</lastBuildDate><atom:link href="https://rselbach.com/tags/model-view/index.xml" rel="self" type="application/rss+xml"/><item><title>Proxy Models Instead of Copied Lists</title><link>https://rselbach.com/proxy-models-instead-of-copied-lists/</link><pubDate>Fri, 06 Jun 2008 18:35:00 +0000</pubDate><guid>https://rselbach.com/proxy-models-instead-of-copied-lists/</guid><description>&lt;p&gt;I added filtering to a Qt 4.3 table by copying matching records into a second list. It worked until an edit in the filtered table changed the copy but not the original. I fixed that by copying changes backward, after which sorting ensured I copied them to the wrong row.&lt;/p&gt;
&lt;p&gt;The obvious approach had created two sources of truth and an index-translation problem. Row seven in the filtered list was not row seven in the source, and after sorting it might not remain row seven in either list. Storing both numbers beside every record only made the confusion durable.&lt;/p&gt;
&lt;p&gt;&lt;code&gt;QSortFilterProxyModel&lt;/code&gt; exists to represent that transformation without duplicating the data. The view uses indexes from the proxy. The proxy maps them to source indexes when it asks for data or applies an edit. Sorting changes the proxy order, while the source model retains its own structure.&lt;/p&gt;
&lt;p&gt;My second mistake was keeping a proxy index for later use. When the filter changed, that index became invalid. A model index is a temporary address into a particular model, not a permanent record identifier. For short-lived view operations, map indexes at the boundary with &lt;code&gt;mapToSource()&lt;/code&gt; or &lt;code&gt;mapFromSource()&lt;/code&gt;. For lasting references, store an identifier from the data and locate it again, or use persistent indexes only when the model can support their semantics correctly.&lt;/p&gt;
&lt;p&gt;Filtering also taught me not to perform expensive parsing in &lt;code&gt;filterAcceptsRow()&lt;/code&gt; on every request. I exposed normalized searchable text through a custom role in the source model and let the proxy compare that. Invalidating the filter after relevant data changes is cheaper than reconstructing a duplicate list and less likely to produce stale rows.&lt;/p&gt;
&lt;p&gt;The model/view mechanism is initially more formal than copying strings into a table, but the formality identifies ownership. The source owns records, the proxy owns presentation order and inclusion, and the view owns selection and display.&lt;/p&gt;
&lt;p&gt;My practical conclusion is to reach for a proxy whenever the same data needs another ordering or subset. Do not copy records merely to make a view convenient. Copies are friendly right up to the first edit, whereupon they become competing historical accounts.&lt;/p&gt;</description></item><item><title>Model/View Without the Fog</title><link>https://rselbach.com/model-view-without-the-fog/</link><pubDate>Thu, 09 Mar 2006 22:06:00 +0000</pubDate><guid>https://rselbach.com/model-view-without-the-fog/</guid><description>&lt;p&gt;I triggered a crash today by deleting the third row of a list. The list had only two visible rows, which was the first clue that my idea of the data and the view&amp;rsquo;s idea had stopped speaking to each other.&lt;/p&gt;
&lt;p&gt;The old code filled a widget item by item and kept pointers to those items in the document. Qt 4&amp;rsquo;s model/view classes ask for a cleaner bargain: the model owns the shape and meaning of the data, while views ask questions through &lt;code&gt;QModelIndex&lt;/code&gt;. That index is a coordinate plus model identity, not the object itself.&lt;/p&gt;
&lt;p&gt;For a flat list, the smallest useful model is not large:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-cpp" data-lang="cpp"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#66d9ef"&gt;class&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;ContactModel&lt;/span&gt; &lt;span style="color:#f92672"&gt;:&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;public&lt;/span&gt; QAbstractListModel
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;{
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#66d9ef"&gt;public&lt;/span&gt;&lt;span style="color:#f92672"&gt;:&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; ContactModel(QObject &lt;span style="color:#f92672"&gt;*&lt;/span&gt;parent &lt;span style="color:#f92672"&gt;=&lt;/span&gt; &lt;span style="color:#ae81ff"&gt;0&lt;/span&gt;)
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;:&lt;/span&gt; QAbstractListModel(parent)
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; }
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;int&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;rowCount&lt;/span&gt;(&lt;span style="color:#66d9ef"&gt;const&lt;/span&gt; QModelIndex &lt;span style="color:#f92672"&gt;&amp;amp;&lt;/span&gt;parent &lt;span style="color:#f92672"&gt;=&lt;/span&gt; QModelIndex()) &lt;span style="color:#66d9ef"&gt;const&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;return&lt;/span&gt; parent.isValid() &lt;span style="color:#f92672"&gt;?&lt;/span&gt; &lt;span style="color:#ae81ff"&gt;0&lt;/span&gt; &lt;span style="color:#f92672"&gt;:&lt;/span&gt; contacts.count();
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; }
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; QVariant &lt;span style="color:#a6e22e"&gt;data&lt;/span&gt;(&lt;span style="color:#66d9ef"&gt;const&lt;/span&gt; QModelIndex &lt;span style="color:#f92672"&gt;&amp;amp;&lt;/span&gt;index, &lt;span style="color:#66d9ef"&gt;int&lt;/span&gt; role) &lt;span style="color:#66d9ef"&gt;const&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;if&lt;/span&gt; (&lt;span style="color:#f92672"&gt;!&lt;/span&gt;index.isValid() &lt;span style="color:#f92672"&gt;||&lt;/span&gt; index.model() &lt;span style="color:#f92672"&gt;!=&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;this&lt;/span&gt; &lt;span style="color:#f92672"&gt;||&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; index.row() &lt;span style="color:#f92672"&gt;&amp;lt;&lt;/span&gt; &lt;span style="color:#ae81ff"&gt;0&lt;/span&gt; &lt;span style="color:#f92672"&gt;||&lt;/span&gt; index.row() &lt;span style="color:#f92672"&gt;&amp;gt;=&lt;/span&gt; contacts.count() &lt;span style="color:#f92672"&gt;||&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; index.column() &lt;span style="color:#f92672"&gt;!=&lt;/span&gt; &lt;span style="color:#ae81ff"&gt;0&lt;/span&gt;)
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;return&lt;/span&gt; QVariant();
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;if&lt;/span&gt; (role &lt;span style="color:#f92672"&gt;==&lt;/span&gt; Qt&lt;span style="color:#f92672"&gt;::&lt;/span&gt;DisplayRole)
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;return&lt;/span&gt; contacts.at(index.row()).name;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;return&lt;/span&gt; QVariant();
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; }
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#66d9ef"&gt;private&lt;/span&gt;&lt;span style="color:#f92672"&gt;:&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; QList&lt;span style="color:#f92672"&gt;&amp;lt;&lt;/span&gt;Contact&lt;span style="color:#f92672"&gt;&amp;gt;&lt;/span&gt; contacts;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;};
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;The parent check is important even for a list. A valid parent asks for children beneath an item; this model has none. Returning the full count there can make a view believe it has discovered a tree of infinite enthusiasm.&lt;/p&gt;
&lt;p&gt;The more interesting part is changing data. The view cannot infer that a row appeared because a &lt;code&gt;QList&lt;/code&gt; grew. The model must announce structural changes around the actual mutation:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-cpp" data-lang="cpp"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#66d9ef"&gt;void&lt;/span&gt; ContactModel&lt;span style="color:#f92672"&gt;::&lt;/span&gt;append(&lt;span style="color:#66d9ef"&gt;const&lt;/span&gt; Contact &lt;span style="color:#f92672"&gt;&amp;amp;&lt;/span&gt;contact)
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;{
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;int&lt;/span&gt; row &lt;span style="color:#f92672"&gt;=&lt;/span&gt; contacts.count();
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; beginInsertRows(QModelIndex(), row, row);
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; contacts.append(contact);
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; endInsertRows();
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;}
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Those calls let attached views adjust selections, editors, and cached geometry coherently. For an ordinary value change, I modify the value and emit &lt;code&gt;dataChanged(index, index)&lt;/code&gt; instead. Resetting the whole model after every edit appears easier, but it discards selection and makes modest updates expensive.&lt;/p&gt;
&lt;p&gt;My crash came from removal code that changed the collection first and emitted signals afterwards. During that gap, a view requested an index using the old shape against the new data. Correct order is not decorative:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-cpp" data-lang="cpp"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#66d9ef"&gt;void&lt;/span&gt; ContactModel&lt;span style="color:#f92672"&gt;::&lt;/span&gt;remove(&lt;span style="color:#66d9ef"&gt;int&lt;/span&gt; row)
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;{
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;if&lt;/span&gt; (row &lt;span style="color:#f92672"&gt;&amp;lt;&lt;/span&gt; &lt;span style="color:#ae81ff"&gt;0&lt;/span&gt; &lt;span style="color:#f92672"&gt;||&lt;/span&gt; row &lt;span style="color:#f92672"&gt;&amp;gt;=&lt;/span&gt; contacts.count())
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;return&lt;/span&gt;;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; beginRemoveRows(QModelIndex(), row, row);
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; contacts.removeAt(row);
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; endRemoveRows();
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;}
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Selections add another wrinkle. A normal &lt;code&gt;QModelIndex&lt;/code&gt; is intended for immediate use. If I must retain an index while rows can move, I use &lt;code&gt;QPersistentModelIndex&lt;/code&gt;, and even then I ask whether storing a domain identifier would be clearer. Persistent indexes survive model notifications, not arbitrary lies told by the model.&lt;/p&gt;
&lt;p&gt;Sorting and filtering belong in a proxy when possible. &lt;code&gt;QSortFilterProxyModel&lt;/code&gt; sits between source and view, translating indexes in both directions. That means code receiving a selected proxy index must map it before addressing source data:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-cpp" data-lang="cpp"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;QModelIndex sourceIndex &lt;span style="color:#f92672"&gt;=&lt;/span&gt; proxy&lt;span style="color:#f92672"&gt;-&amp;gt;&lt;/span&gt;mapToSource(view&lt;span style="color:#f92672"&gt;-&amp;gt;&lt;/span&gt;currentIndex());
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;Contact contact &lt;span style="color:#f92672"&gt;=&lt;/span&gt; model&lt;span style="color:#f92672"&gt;-&amp;gt;&lt;/span&gt;contactAt(sourceIndex.row());
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Forgetting this works beautifully until sorting changes the order. These are the bugs that wait politely through a demonstration and introduce themselves to the first user.&lt;/p&gt;
&lt;p&gt;I prefer models that expose domain data through roles and operations, with widgets knowing as little as possible about storage. One model can then drive a list, a combo box, and perhaps a custom view without three synchronisation routines. The caveat is that model/view has real ceremony. A fixed list of five labels does not need a heroic subclass; &lt;code&gt;QStringListModel&lt;/code&gt; may be enough.&lt;/p&gt;
&lt;p&gt;Editable models need one more honest contract. &lt;code&gt;flags()&lt;/code&gt; must advertise editable indexes, and &lt;code&gt;setData()&lt;/code&gt; must validate the role and value before changing storage. Returning &lt;code&gt;true&lt;/code&gt; means the edit happened, not merely that the method received a pleasant visit:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-cpp" data-lang="cpp"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#66d9ef"&gt;bool&lt;/span&gt; ContactModel&lt;span style="color:#f92672"&gt;::&lt;/span&gt;setData(&lt;span style="color:#66d9ef"&gt;const&lt;/span&gt; QModelIndex &lt;span style="color:#f92672"&gt;&amp;amp;&lt;/span&gt;index,
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;const&lt;/span&gt; QVariant &lt;span style="color:#f92672"&gt;&amp;amp;&lt;/span&gt;value, &lt;span style="color:#66d9ef"&gt;int&lt;/span&gt; role)
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;{
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;if&lt;/span&gt; (role &lt;span style="color:#f92672"&gt;!=&lt;/span&gt; Qt&lt;span style="color:#f92672"&gt;::&lt;/span&gt;EditRole &lt;span style="color:#f92672"&gt;||&lt;/span&gt; &lt;span style="color:#f92672"&gt;!&lt;/span&gt;index.isValid() &lt;span style="color:#f92672"&gt;||&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; index.model() &lt;span style="color:#f92672"&gt;!=&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;this&lt;/span&gt; &lt;span style="color:#f92672"&gt;||&lt;/span&gt; index.row() &lt;span style="color:#f92672"&gt;&amp;lt;&lt;/span&gt; &lt;span style="color:#ae81ff"&gt;0&lt;/span&gt; &lt;span style="color:#f92672"&gt;||&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; index.row() &lt;span style="color:#f92672"&gt;&amp;gt;=&lt;/span&gt; contacts.count() &lt;span style="color:#f92672"&gt;||&lt;/span&gt; index.column() &lt;span style="color:#f92672"&gt;!=&lt;/span&gt; &lt;span style="color:#ae81ff"&gt;0&lt;/span&gt;)
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;return&lt;/span&gt; false;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; contacts[index.row()].name &lt;span style="color:#f92672"&gt;=&lt;/span&gt; value.toString();
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; emit &lt;span style="color:#a6e22e"&gt;dataChanged&lt;/span&gt;(index, index);
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;return&lt;/span&gt; true;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;}
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;The model and bounds checks matter here too. An index from another model is not permission to address this model&amp;rsquo;s storage, even when its row happens to fit. The delegate asks the model to commit; it does not gain ownership of the underlying record.&lt;/p&gt;
&lt;p&gt;There is also a testing advantage. I can exercise &lt;code&gt;rowCount&lt;/code&gt;, &lt;code&gt;data&lt;/code&gt;, insertion, and removal with &lt;code&gt;QCoreApplication&lt;/code&gt;, no screen required. My current test records the emitted row ranges and checks the resulting names. It is not glamorous, but neither is debugging a stale selection at midnight.&lt;/p&gt;
&lt;p&gt;Qt 4&amp;rsquo;s mechanism got less foggy when I stopped treating the model as a collection adapter. The view trusts its signals and invariants. Once I stopped lying to it, the disappearing third row quit crashing, though it remains philosophically ambitious.&lt;/p&gt;</description></item></channel></rss>