Skip to content

Latest commit

 

History

History
1383 lines (1055 loc) · 52.5 KB

File metadata and controls

1383 lines (1055 loc) · 52.5 KB

五、数据

在本章中,我们将实现类来处理任何业务线应用中最关键的部分——数据。我们将介绍自感知数据实体,它可以自动序列化往返于 JavaScript 对象符号 ( JSON )这是一种在网络通信中经常使用的流行序列化格式。我们将为我们的应用创建我们需要的核心模型,并将它们连接到我们的用户界面,以便通过自定义控件进行读写。我们将涵盖以下主题:

  • JSON
  • 数据装饰者
  • 抽象数据实体
  • 数据实体的集合
  • 具体的数据模型
  • 用户界面控件和数据绑定

JSON

如果你以前从未遇到过 JSON,让我们来一个快速速成课程。这是一种表达对象层次结构及其属性的简单而轻量的方法。在 HTTP 请求中发送数据时,这是一个非常流行的选择。它在意图上类似于 XML,但是不太冗长。

JSON 对象被封装在大括号{}中,而属性用格式键表示:值。字符串用双引号""分隔。我们可以如下表示单个客户端对象:

{
    "reference": "CLIENT0001",
    "name": "Dale Cooper"
}

请注意,空格和控制字符(如制表符和换行符)会被忽略,缩进属性只是为了使内容更易读。

It's usually a good idea to strip extraneous characters out of JSON when transmitting over the network (for example, in an HTTP request) in order to reduce the size of the payload; every byte counts!

属性值可以是以下类型之一:StringNumberJSON ObjectJSON Array,以及文字值truefalsenull

我们可以将供应地址和账单地址作为子 JSON 对象添加到我们的客户端,为每个对象提供一个唯一的密钥。虽然密钥可以是任何格式,只要它们是唯一的,但通常的做法是使用 camel case,例如,myAwesomeJsonKey。我们可以用 null 表示一个空地址对象:

{
    "reference": "CLIENT0001",
    "name": "Dale Cooper",
    "supplyAddress": {
         "number": 7,
        "name": "White Lodge",
        "street": "Lost Highway",
        "city": "Twin Peaks",
        "postcode": "WS119"
    },
    "billingAddress": null
}

对象的集合(数组)用方括号[]括起来,用逗号隔开。我们可以简单地将方括号留空来表示没有预约:

{
    "reference": "CLIENT0001",
    "name": "Dale Cooper",
    "supplyAddress": {
        "number": 7,
        "name": "White Lodge",
        "street": "Lost Highway",
        "city": "Twin Peaks",
        "postcode": "WS119"
    },
    "billingAddress": null,
    "contacts": [
        {
            "type": 1,
            "address": "+12345678"
        },
        {
            "type": 2,
            "address": "dale.cooper@fbi.com"
        }
    ],
    "appointments": []
}

对象层次结构

大多数现实世界的应用以层次或关系的方式表示数据,数据被合理化为离散的对象。通常有一个中心“根”对象,它是其他几个子对象的父对象,或者作为单个对象,或者作为集合。每个离散对象都有自己的一组数据项,可以是任意数量的类型。我们想要涵盖的主要原则如下:

  • 一系列数据类型(stringintegerdatetime)和一个枚举值
  • 对象层次结构
  • 同一类型的多个单一子实体
  • 实体集合

在简单性与这些目标之间取得平衡,我们将努力实现的数据图表如下:

下表描述了这些模型的用途:

| 车型 | 描述 | | 客户端 | 这是我们对象层次结构的根,代表我们公司与之有关系的个人或团体,例如客户或患者。 | | 联系 | 我们可以用来联系客户的地址集合。可能的联系方式有电话、电子邮件和传真。每个客户端可能有一个或多个联系人。 | | 预约 | 与客户的预约集合,例如,现场参观或咨询。每个客户可能有零个或多个约会。 | | 供应地址 | 与客户关系的核心地址,例如,我们公司为患者提供能源的地点或家庭地址。每个客户端必须有一个供应地址。 | | 计费地址 | 一个可选地址,不同于用于开票的供应地址,例如,公司的总部。每个客户端可能有零个或一个账单地址。 |

Another perfectly valid approach would be to aggregate the addresses into a collection, much like we have done with our contacts, but I want to demonstrate using the same type of object (Address) in multiple properties.

有了高级设计,我们现在可以编写我们的类了。然而,在我们开始我们的数据实体之前,让我们看一下数据项。

数据装饰器

我们的客户端模型的name属性的一个简单实现是将其添加为QString;然而,这种方法有一些缺点。每当我们在用户界面中显示这个属性时,我们可能会希望在文本框旁边显示一个信息标签,以便用户知道它是干什么的,比如说“名称”或类似的东西。每当我们想要验证用户输入的名称时,我们必须在其他地方的代码中管理它。最后,如果我们想将值序列化到 JSON 或从 JSON 序列化,同样需要一些其他组件来为我们实现。

为了解决所有这些问题,我们将引入DataDecorator的概念,它将提升给定的基础数据类型,并为我们提供标签、验证功能和开箱即用的 JSON 序列化。我们的模型将维护DataDecorators的集合,允许它们通过简单地遍历数据项并执行相关的动作来验证自己并将其序列化为 JSON。

在我们的cm-lib项目中,在一个新文件夹cm-lib/source/data中创建以下类:

| | 目的 | | DataDecorator | 数据项的基类 | | StringDecorator | 字符串属性的派生类 | | IntDecorator | 整数属性的派生类 | | DateTimeDecorator | 日期/时间属性的派生类 | | EnumeratorDecorator | 枚举属性的派生类 |

我们的DataDecorator基类将包含所有数据项共享的特性。

data-decorator.h:

#ifndef DATADECORATOR_H
#define DATADECORATOR_H

#include <QJsonObject>
#include <QJsonValue>
#include <QObject>
#include <QScopedPointer>

#include <cm-lib_global.h>

namespace cm {
namespace data {

class Entity;

class CMLIBSHARED_EXPORT DataDecorator : public QObject
{
    Q_OBJECT
    Q_PROPERTY( QString ui_label READ label CONSTANT )

public:
    DataDecorator(Entity* parent = nullptr, const QString& key = 
                  "SomeItemKey", const QString& label = "");
                                 virtual ~DataDecorator();

    const QString& key() const;
    const QString& label() const;
    Entity* parentEntity();

    virtual QJsonValue jsonValue() const = 0;
    virtual void update(const QJsonObject& jsonObject) = 0;

private:
    class Implementation;
    QScopedPointer<Implementation> implementation;
};

}}

#endif

我们从 QObject 继承,添加我们的dllexport宏,并像往常一样将整个内容包装在名称空间中。此外,因为这是一个抽象基类,我们确保实现了一个虚拟析构函数。

我们知道,因为我们从 QObject 继承,所以我们希望在构造函数中接收一个指向父对象的指针。我们还知道,所有数据项都将是一个实体的子代(我们将很快编写该实体,并在此进行转发声明),该实体本身将从 QObject 派生。我们可以利用这两个事实将我们的DataDecorator直接父化为一个实体。

我们用几根线构造装饰器。我们所有的数据装饰器都必须有一个在序列化到 JSON 和从 JSON 序列化时使用的键,并且它们还将共享一个label属性,用户界面可以使用该属性在数据控件旁边显示描述性文本。我们将这些成员藏在私有实现中,并为他们实现一些访问器方法。

最后,我们开始实现我们的 JSON 序列化,通过声明虚拟方法将值表示为QJsonValue并从提供的QJsonObject更新值。由于该值在基类中是未知的,而是将在派生类中实现,因此这两种方法都是纯虚函数。

data-decorator.cpp:

#include "data-decorator.h"

namespace cm {
namespace data {

class DataDecorator::Implementation
{
public:
    Implementation(Entity* _parent, const QString& _key, const QString& 
                                                         _label)
        : parentEntity(_parent)
        , key(_key)
        , label(_label)
    {
    }
    Entity* parentEntity{nullptr};
    QString key;
    QString label;
};

DataDecorator::DataDecorator(Entity* parent, const QString& key, const QString& label)
    : QObject((QObject*)parent)
{
    implementation.reset(new Implementation(parent, key, label));
}

DataDecorator::~DataDecorator()
{
}

const QString& DataDecorator::key() const
{
    return implementation->key;
}

const QString& DataDecorator::label() const
{
    return implementation->label;
}

Entity* DataDecorator::parentEntity()
{
    return implementation->parentEntity;
}

}}

实现非常简单,本质上只是管理一些数据成员。

接下来,我们将实现处理字符串的派生装饰器类。

string-decorator.h:

#ifndef STRINGDECORATOR_H
#define STRINGDECORATOR_H

#include <QJsonObject>
#include <QJsonValue>
#include <QObject>
#include <QScopedPointer>
#include <QString>

#include <cm-lib_global.h>
#include <data/data-decorator.h>

namespace cm {
namespace data {

class CMLIBSHARED_EXPORT StringDecorator : public DataDecorator
{
    Q_OBJECT

    Q_PROPERTY( QString ui_value READ value WRITE setValue NOTIFY 
               valueChanged )
public:
    StringDecorator(Entity* parentEntity = nullptr, const QString& key = "SomeItemKey", const QString& label = "", const QString& value = "");
    ~StringDecorator();

    StringDecorator& setValue(const QString& value);
    const QString& value() const;

    QJsonValue jsonValue() const override;
    void update(const QJsonObject& jsonObject) override;

signals:
    void valueChanged();

private:
    class Implementation;
    QScopedPointer<Implementation> implementation;
};

}}

#endif

这里没有太多其他事情发生——我们只是添加了一个强类型的QString值属性来保存我们的值。我们还覆盖了虚拟 JSON 相关的方法。

When deriving from a class that inherits from QObject, you need to add the Q_OBJECT macro to the derived class as well as the base class if the derived class implements its own signals or slots.

string-decorator.cpp:

#include "string-decorator.h"

#include <QVariant>

namespace cm {
namespace data {

class StringDecorator::Implementation
{
public:
    Implementation(StringDecorator* _stringDecorator, const QString& 
                                                      _value)
        : stringDecorator(_stringDecorator)
        , value(_value)
    {
    }

    StringDecorator* stringDecorator{nullptr};
    QString value;
};

StringDecorator::StringDecorator(Entity* parentEntity, const QString& key, const QString& label, const QString& value)
    : DataDecorator(parentEntity, key, label)
{
    implementation.reset(new Implementation(this, value));
}

StringDecorator::~StringDecorator()
{
}

const QString& StringDecorator::value() const
{
    return implementation->value;
}

StringDecorator& StringDecorator::setValue(const QString& value)
{
    if(value != implementation->value) {
        // ...Validation here if required...
        implementation->value = value;
        emit valueChanged();
    }
    return *this;
}

QJsonValue StringDecorator::jsonValue() const
{
    return QJsonValue::fromVariant(QVariant(implementation->value));
}

void StringDecorator::update(const QJsonObject& _jsonObject)
{
    if (_jsonObject.contains(key())) {
        setValue(_jsonObject.value(key()).toString());
    } else {
        setValue("");
    }
}
}}

同样,这里没有什么特别复杂的。通过使用READWRITE属性语法,而不是更简单的MEMBER关键字,我们现在有了一种拦截用户界面设置的值的方法,并且我们可以决定是否要将更改应用于成员变量。变异器可以像您需要的那样复杂,但是我们现在所做的只是设置值并发出信号告诉用户界面它已经被改变了。我们在相等检查中包装操作,因此如果新值与旧值相同,我们不会采取任何操作。

Here, the mutator returns a reference to self (*this), which is helpful because it enables method chaining, for example,  myName.setValue(“Nick”).setSomeNumber(1234).setSomeOtherProperty(true). However, this is not necessary for the property bindings, so feel free to use the more common void return type if you prefer.

我们使用两步转换过程,先将我们的QString值转换为QVariant,然后再将其转换为我们的目标QJsonValue类型。将使用DataDecorator基类的keyQJsonValue插入父实体 JSON 对象。当我们编写实体相关类时,我们将更详细地介绍这一点。

An alternative approach would be to simply represent the value of our various data items as a QVariant member in the DataDecorator base class, removing the need to have separate classes for QString, int, and so on. The problem with this approach is that you end up having to write lots of nasty code that says “if you have a QVariant containing a string then run this code if it contains an int then run this code...”. I prefer the additional overhead of writing the extra classes in exchange for having known types and cleaner, simpler code. This will become particularly helpful when we look at data validation. Validating a string is completely different from validating a number and different again from validating a date.

IntDecoratorDateTimeDecorator实际上与StringDecorator相同,只是将QString值替换为 int 或QDateTime。不过我们可以补充DateTimeDecorator一些附加属性来帮助我们。添加以下属性和与之对应的访问器方法:

Q_PROPERTY( QString ui_iso8601String READ toIso8601String NOTIFY valueChanged )
Q_PROPERTY( QString ui_prettyDateString READ toPrettyDateString NOTIFY valueChanged )
Q_PROPERTY( QString ui_prettyTimeString READ toPrettyTimeString NOTIFY valueChanged )
Q_PROPERTY( QString ui_prettyString READ toPrettyString NOTIFY valueChanged )

这些属性的目的是使用户界面能够方便地访问日期/时间值,作为预格式化为几种不同样式的QString。让我们遍历每个访问器的实现。

Qt 内置了对 ISO8601 格式日期的支持,这是在系统之间传输日期时间值时非常常见的格式,例如在 HTTP 请求中。它是一种灵活的格式,支持几种不同的表示,但通常遵循格式 yyyy-MM-ddTHH:mm:ss.zt,其中 T 是字符串文字,z 是毫秒,T 是时区信息:

QString DateTimeDecorator::toIso8601String() const
{
    if (implementation->value.isNull()) {
        return "";
    } else {
        return implementation->value.toString(Qt::ISODate);
    }
}

接下来,我们提供一种以人类可读的长格式显示完整日期时间的方法,例如,2017 年 7 月 22 日星期六@ 12:07:45:

QString DateTimeDecorator::toPrettyString() const
{
    if (implementation->value.isNull()) {
        return "Not set";
    } else {
        return implementation->value.toString( "ddd d MMM yyyy @ HH:mm:ss" );
    }
}

最后两种方法显示日期或时间部分,例如,2017 年 7 月 22 日或下午 12:07:

QString DateTimeDecorator::toPrettyDateString() const
{
    if (implementation->value.isNull()) {
        return "Not set";
    } else {
        return implementation->value.toString( "d MMM yyyy" );
    }
}

QString DateTimeDecorator::toPrettyTimeString() const
{
    if (implementation->value.isNull()) {
        return "Not set";
    } else {
        return implementation->value.toString( "hh:mm ap" );
    }
}

我们的最终类型EnumeratorDecoratorIntDecorator大致相同,但它也接受映射器。这个容器帮助我们将存储的 int 值映射到字符串表示。如果我们考虑我们计划实现的Contact.type枚举器,枚举值将是 0、1、2 等等;然而,当涉及到 UI 时,这个数字对用户来说没有任何意义。我们真的需要呈现EmailTelephone,或者一些其他的字符串表示,地图允许我们这样做。

enumerator-decorator.h:

#ifndef ENUMERATORDECORATOR_H
#define ENUMERATORDECORATOR_H

#include <map>

#include <QJsonObject>
#include <QJsonValue>
#include <QObject>
#include <QScopedPointer>

#include <cm-lib_global.h>
#include <data/data-decorator.h>

namespace cm {
namespace data {

class CMLIBSHARED_EXPORT EnumeratorDecorator : public DataDecorator
{
    Q_OBJECT
    Q_PROPERTY( int ui_value READ value WRITE setValue NOTIFY 
                                              valueChanged )
    Q_PROPERTY( QString ui_valueDescription READ valueDescription 
                                             NOTIFY valueChanged )

public:
    EnumeratorDecorator(Entity* parentEntity = nullptr, const QString& 
    key = "SomeItemKey", const QString& label = "", int value = 0,  
    const std::map<int, QString>& descriptionMapper = std::map<int, 
     QString>());
    ~EnumeratorDecorator();

    EnumeratorDecorator& setValue(int value);
    int value() const;
    QString valueDescription() const;

    QJsonValue jsonValue() const override;
    void update(const QJsonObject& jsonObject) override;

signals:
    void valueChanged();

private:
    class Implementation;
    QScopedPointer<Implementation> implementation;
};

}}

#endif

我们将映射作为另一个成员变量存储在私有实现类中,然后使用它来提供枚举值的字符串表示:

QString EnumeratorDecorator::valueDescription() const
{
    if (implementation->descriptionMapper.find(implementation->value) 
                       != implementation->descriptionMapper.end()) {
        return implementation->descriptionMapper.at(implementation-
                                                    >value);
    } else {
        return {};
    }
}

既然我们已经介绍了实体所需的数据类型,那么让我们继续讨论实体本身。

实体

因为我们有很多功能想要在我们的数据模型中共享,我们将实现一个实体基类。我们需要能够表示父/子关系,以便客户可以有供应和账单地址。我们还需要为我们的联系人和约会支持实体集合。最后,每个实体层次结构必须能够将自己序列化到 JSON 对象和从 JSON 对象序列化。

cm-lib/source/data中创建新的类实体。

entity.h:

#ifndef ENTITY_H
#define ENTITY_H

#include <map>

#include <QObject>
#include <QScopedPointer>

#include <cm-lib_global.h>
#include <data/data-decorator.h>

namespace cm {
namespace data {

class CMLIBSHARED_EXPORT Entity : public QObject
{
    Q_OBJECT

public:
    Entity(QObject* parent = nullptr, const QString& key = 
                                                  "SomeEntityKey");
    Entity(QObject* parent, const QString& key, const QJsonObject& 
     jsonObject);
    virtual ~Entity();

public:
    const QString& key() const;
    void update(const QJsonObject& jsonObject);
    QJsonObject toJson() const;

signals:
    void childEntitiesChanged();
    void dataDecoratorsChanged();

protected:
    Entity* addChild(Entity* entity, const QString& key);
    DataDecorator* addDataItem(DataDecorator* dataDecorator);

protected:
    class Implementation;
    QScopedPointer<Implementation> implementation;
};

}}

#endif

entity.cpp:

#include "entity.h"

namespace cm {
namespace data {

class Entity::Implementation
{
public:
    Implementation(Entity* _entity, const QString& _key)
        : entity(_entity)
        , key(_key)
    {
    }
    Entity* entity{nullptr};
    QString key;
    std::map<QString, Entity*> childEntities;
    std::map<QString, DataDecorator*> dataDecorators;
};

Entity::Entity(QObject* parent, const QString& key)
    : QObject(parent)
{
    implementation.reset(new Implementation(this, key));
}

Entity::Entity(QObject* parent, const QString& key, const QJsonObject& 
               jsonObject) : Entity(parent, key)
{
    update(jsonObject);
}

Entity::~Entity()
{
}

const QString& Entity::key() const
{
    return implementation->key;
}

Entity* Entity::addChild(Entity* entity, const QString& key)
{
    if(implementation->childEntities.find(key) == 
        std::end(implementation->childEntities)) {
        implementation->childEntities[key] = entity;
        emit childEntitiesChanged();
    }
    return entity;
}

DataDecorator* Entity::addDataItem(DataDecorator* dataDecorator)
{
    if(implementation->dataDecorators.find(dataDecorator->key()) == 
       std::end(implementation->dataDecorators)) {
        implementation->dataDecorators[dataDecorator->key()] = 
        dataDecorator;
        emit dataDecoratorsChanged();
    }
    return dataDecorator;
}

void Entity::update(const QJsonObject& jsonObject)
{
    // Update data decorators
    for (std::pair<QString, DataDecorator*> dataDecoratorPair : 
         implementation->dataDecorators) {
        dataDecoratorPair.second->update(jsonObject);
    }
    // Update child entities
    for (std::pair<QString, Entity*> childEntityPair : implementation-
    >childEntities) {childEntityPair.second>update(jsonObject.value(childEntityPair.first).toObject());
    }
}

QJsonObject Entity::toJson() const
{
    QJsonObject returnValue;
    // Add data decorators
    for (std::pair<QString, DataDecorator*> dataDecoratorPair : 
                         implementation->dataDecorators) {
        returnValue.insert( dataDecoratorPair.first, 
        dataDecoratorPair.second->jsonValue() );
    }
    // Add child entities
    for (std::pair<QString, Entity*> childEntityPair : implementation->childEntities) {
        returnValue.insert( childEntityPair.first, childEntityPair.second->toJson() );
    }
    return returnValue;
}

}}

很像我们的DataDecorator基类,我们给所有实体分配一个唯一的键,它将用于 JSON 序列化。我们还添加了一个重载的构造函数,我们可以向它传递一个QJsonObject,这样我们就可以从 JSON 实例化一个实体。与此相关的是,我们还声明了一对方法来将现有实例序列化到 JSON 和从 JSON 序列化。

我们的实体将维护一些集合——表示模型属性的数据装饰器的映射,以及表示单个子代的实体的映射。我们将每个项目的键映射到实例。

我们公开了几个受保护的方法,这些方法是派生类将用来添加它的数据项和子对象;例如,我们的客户端模型将添加一个名称数据项以及supplyAddressbillingAddress子级。为了补充这些方法,我们还添加了信号来告诉任何感兴趣的观察者集合已经改变。

在这两种情况下,我们都会在添加之前检查该键在地图上是否已经存在。然后,我们返回提供的指针,以便消费者可以使用它进行进一步的操作。当我们开始实现数据模型时,您会看到它的价值。

我们将我们填充的映射用于 JSON 序列化方法。我们已经在我们的DataDecorator基类上声明了一个update()方法,所以我们简单地遍历所有数据项,并将 JSON 对象依次传递给每个数据项。每个派生装饰器类都有自己的实现来处理解析。类似地,我们在每个子实体上递归调用Entity::update()

序列化为 JSON 对象遵循相同的模式。每个数据项都可以将其值转换为QJsonValue对象,所以我们依次获取每个值,并使用每个数据项的键将其追加到一个根 JSON 对象中。我们在每个子节点上递归调用Entity::toJson(),这沿着层次树向下级联。

在我们完成我们的实体之前,我们需要声明一组类来表示一个实体集合。

实体集合

为了实现实体集合,我们需要利用一些更高级的 C++ 技术,到目前为止,我们将暂时打破常规,在一个头文件中实现多个类。

cm-lib/source/data中创建entity-collection.h,并在其中添加我们的名称空间作为普通名称空间,并转发声明实体:

#ifndef ENTITYCOLLECTION_H
#define ENTITYCOLLECTION_H

namespace cm {
namespace data {
    class Entity;
}}

#endif

接下来,我们将依次浏览必要的类,每个类都必须按顺序添加到名称空间中。

我们首先定义根类,它只不过是从QObject继承,并让我们获得它带来的所有好处,比如对象所有权和信号。这是必需的,因为直接从QObject派生的类不能模板化:

class CMLIBSHARED_EXPORT EntityCollectionObject : public QObject
{
    Q_OBJECT

public:
    EntityCollectionObject(QObject* _parent = nullptr) : QObject(_parent) {}
    virtual ~EntityCollectionObject() {}

signals:
    void collectionChanged();
};

您需要为QObject和我们的 DLL 导出宏添加包含。接下来,我们需要一个类型不可知的接口来使用我们的实体,就像我们已经实现的DataDecorator和实体映射一样。然而,这里的事情有点复杂,因为我们不会为我们拥有的每个集合派生一个新的类,所以我们需要一些获取类型化数据的方法。我们有两个要求。首先,用户界面需要一个派生类型的QList(例如客户端 *),这样它就可以访问特定于一个客户端的所有属性并显示所有数据。其次,我们的实体类需要一个基类型的向量(实体 *),这样它就可以迭代它的集合,而不需要考虑它处理的到底是哪种类型。我们实现这一点的方法是声明两个模板方法,但是将它们的定义推迟到以后。当消费者想要派生类型的集合时将使用derivedEntities(),而当消费者只想访问基本接口时将使用baseEntities():

class EntityCollectionBase : public EntityCollectionObject
{
public:
    EntityCollectionBase(QObject* parent = nullptr, const QString& key 
                                         = "SomeCollectionKey")
        : EntityCollectionObject(parent)
        , key(key)
    {}

    virtual ~EntityCollectionBase()
    {}

    QString getKey() const
    {
        return key;
    }

    virtual void clear() = 0;
    virtual void update(const QJsonArray& json) = 0;
    virtual std::vector<Entity*> baseEntities() = 0;

    template <class T>
    QList<T*>& derivedEntities();

    template <class T>
    T* addEntity(T* entity);

private:
    QString key;
};

接下来,我们声明一个完整的模板类,在其中存储派生类型的集合并实现所有方法,除了我们刚刚讨论的两个模板方法:

template <typename T>
class EntityCollection : public EntityCollectionBase
{
public:
    EntityCollection(QObject* parent = nullptr, const QString& key = 
             "SomeCollectionKey")
        : EntityCollectionBase(parent, key)
    {}

    ~EntityCollection()
    {}

    void clear() override
    {
        for(auto entity : collection) {
            entity->deleteLater();
        }
        collection.clear();
    }

    void update(const QJsonArray& jsonArray) override
    {
        clear();
        for(const QJsonValue& jsonValue : jsonArray) {
            addEntity(new T(this, jsonValue.toObject()));
        }
    }

    std::vector<Entity*> baseEntities() override
    {
        std::vector<Entity*> returnValue;
        for(T* entity : collection) {
            returnValue.push_back(entity);
        }
        return returnValue;
    }

    QList<T*>& derivedEntities()
    {
        return collection;
    }

    T* addEntity(T* entity)
    {
        if(!collection.contains(entity)) {
            collection.append(entity);
            EntityCollectionObject::collectionChanged();
        }
        return entity;
    }

private:
    QList<T*> collection;       
};

You will need #include <QJsonValue> and <QJsonArray> for these classes.

clear()方法只是清空集合,整理内存;update()在概念上与我们在 Entity 中实现的 JSON 方法相同,只是我们处理的是实体的集合,所以我们取一个 JSON 数组而不是一个对象。addEntity()向集合中添加一个派生类的实例,derivedEntities()返回集合;baseEntities()做更多的工作,根据请求创建一个新的向量,并用集合中的所有项目填充它。它只是隐式转换指针,所以我们不关心昂贵的对象实例化。

最后,我们提供了我们神奇的模板化方法的实现:

template <class T>
QList<T*>& EntityCollectionBase::derivedEntities()
{
    return dynamic_cast<const EntityCollection<T>&>(*this).derivedEntities();
}

template <class T>
T* EntityCollectionBase::addEntity(T* entity)
{
    return dynamic_cast<const EntityCollection<T>&>(*this).addEntity(entity);
}

通过延迟这些方法的实现,我们已经实现了我们的模板化EntityCollection类。我们现在可以将对模板化方法的任何调用“路由”到模板化类中的实现。这是一种复杂的技巧,但是当我们开始在现实模型中实现这些集合时,它将会更有意义。

现在我们的实体集合已经准备好了,我们可以返回到我们的实体类,并将它们添加到组合中。

在标题#include <data/entity-collection.h>中,添加信号:

void childCollectionsChanged(const QString& collectionKey);

另外,添加受保护的方法:

EntityCollectionBase* addChildCollection(EntityCollectionBase* entityCollection);

在实现文件中,添加私有成员:

std::map<QString, EntityCollectionBase*> childCollections;

然后,添加方法:

EntityCollectionBase* Entity::addChildCollection(EntityCollectionBase* entityCollection)
{
    if(implementation->childCollections.find(entityCollection- 
     >getKey()) == std::end(implementation->childCollections)) {
        implementation->childCollections[entityCollection->getKey()] =  
                                        entityCollection;
        emit childCollectionsChanged(entityCollection->getKey());
    }
    return entityCollection;
}

这与其他映射的工作方式完全相同,将键与指向基类的指针相关联。

接下来,将集合添加到update()方法中:

void Entity::update(const QJsonObject& jsonObject)
{
    // Update data decorators
    for (std::pair<QString, DataDecorator*> dataDecoratorPair :   
         implementation->dataDecorators) {
        dataDecoratorPair.second->update(jsonObject);
    }

    // Update child entities
    for (std::pair<QString, Entity*> childEntityPair : implementation- 
       >childEntities) { childEntityPair.second- 
       >update(jsonObject.value(childEntityPair.first).toObject());
    }

    // Update child collections
    for (std::pair<QString, EntityCollectionBase*> childCollectionPair 
         : implementation->childCollections) {
            childCollectionPair.second-
        >update(jsonObject.value(childCollectionPair.first).toArray());
    }
}

最后,将集合添加到toJson()方法中:

QJsonObject Entity::toJson() const
{
    QJsonObject returnValue;

    // Add data decorators
    for (std::pair<QString, DataDecorator*> dataDecoratorPair : 
        implementation->dataDecorators) {
        returnValue.insert( dataDecoratorPair.first, 
        dataDecoratorPair.second->jsonValue() );
    }

    // Add child entities
    for (std::pair<QString, Entity*> childEntityPair : implementation-
        >childEntities) {
        returnValue.insert( childEntityPair.first, 
       childEntityPair.second->toJson() );
    }

    // Add child collections
    for (std::pair<QString, EntityCollectionBase*> childCollectionPair 
        : implementation->childCollections) {
        QJsonArray entityArray;
            for (Entity* entity : childCollectionPair.second-
           >baseEntities()) {
            entityArray.append( entity->toJson() );
        }
        returnValue.insert( childCollectionPair.first, entityArray );
    }

    return returnValue;
}

You will need #include <QJsonArray> for that last snippet.

我们用baseEntities()方法给我们集合Entity*。然后,我们将每个实体的 JSON 对象追加到一个 JSON 数组中,完成后,用集合的键将该数组添加到我们的根 JSON 对象中。

过去的几节非常长而且复杂,可能看起来只是为了实现一些数据模型而做了很多工作。然而,所有的代码都是你一次编写的,它为你提供了很多免费的功能,无论你继续做什么,所以从长远来看,这是值得的。我们将继续研究如何在我们的数据模型中实现这些类。

数据模型

现在,我们已经有了能够定义数据对象(实体和实体集合)和各种类型的属性(数据装饰器)的基础设施,我们可以继续前进,构建我们在本章前面介绍的对象层次结构。我们已经有了一个由 Qt Creator 创建的默认客户端类,所以在cm-lib/source/models中用以下新类来补充它:

| | 目的 | | Address | 表示供应或计费地址 | | Appointment | 代表与客户的约会 | | Contact | 表示联系客户端的方法 |

我们将从最简单的模型开始——地址。

address.h:

#ifndef ADDRESS_H
#define ADDRESS_H

#include <QObject>

#include <cm-lib_global.h>
#include <data/string-decorator.h>
#include <data/entity.h>

namespace cm {
namespace models {

class CMLIBSHARED_EXPORT Address : public data::Entity
{
    Q_OBJECT
    Q_PROPERTY(cm::data::StringDecorator* ui_building MEMBER building 
                                                      CONSTANT)
    Q_PROPERTY(cm::data::StringDecorator* ui_street MEMBER street  
                                                    CONSTANT)
    Q_PROPERTY(cm::data::StringDecorator* ui_city MEMBER city CONSTANT)
    Q_PROPERTY(cm::data::StringDecorator* ui_postcode MEMBER postcode 
                                                      CONSTANT)
    Q_PROPERTY(QString ui_fullAddress READ fullAddress CONSTANT)

public:
    explicit Address(QObject* parent = nullptr);
    Address(QObject* parent, const QJsonObject& json);

    data::StringDecorator* building{nullptr};
    data::StringDecorator* street{nullptr};
    data::StringDecorator* city{nullptr};
    data::StringDecorator* postcode{nullptr};

    QString fullAddress() const;
};

}}

#endif

我们定义了在本章开始时设计的属性,但是我们没有使用常规的QString对象,而是使用了新的StringDecorators。为了保护数据的完整性,我们应该使用READ关键字,并通过访问器方法返回一个StringDecorator* const,但是为了简单起见,我们将使用MEMBER来代替。我们还提供了一个重载的构造函数,可以用来从QJsonObject构造地址。最后,我们添加一个助手fullAddress()方法和属性,将地址元素连接成一个字符串,供用户界面使用。

address.cpp:

#include "address.h"

using namespace cm::data;

namespace cm {
namespace models {

Address::Address(QObject* parent)
        : Entity(parent, "address")
{
    building = static_cast<StringDecorator*>(addDataItem(new StringDecorator(this, "building", "Building")));
    street = static_cast<StringDecorator*>(addDataItem(new StringDecorator(this, "street", "Street")));
    city = static_cast<StringDecorator*>(addDataItem(new StringDecorator(this, "city", "City")));
    postcode = static_cast<StringDecorator*>(addDataItem(new StringDecorator(this, "postcode", "Post Code")));
}

Address::Address(QObject* parent, const QJsonObject& json)
        : Address(parent)
{
    update(json);
}

QString Address::fullAddress() const
{
    return building->value() + " " + street->value() + "\n" + city->value() + "\n" + postcode->value();
}

}}

这就是我们所有努力工作的开始。我们需要对每个属性做两件事。首先,我们需要一个指向派生类型(StringDecorator)的指针,我们可以将其呈现给用户界面,以便显示和编辑该值。其次,我们需要让基本实体类知道基本类型(DataDecorator),这样它就可以迭代数据项并为我们执行 JSON 序列化工作。我们可以使用addDataItem()方法在一句话中实现这两个目标:

building = static_cast<StringDecorator*>(addDataItem(new StringDecorator(this, "building", "Building")));

分解后,我们用building键和Building用户界面标签创建一个新的StringDecorator*。这将立即传递给addDataItem(),T3 将其添加到实体中的dataDecorators集合中,并将数据项作为DataDecorator*返回。然后,我们可以将其转换回StringDecorator*,然后将其存储在building成员变量中。

这里唯一的另一个实现是获取一个 JSON 对象,通过调用默认构造函数来正常构造地址,然后使用update()方法更新模型。

AppointmentContact模型遵循相同的模式,只是它们的数据类型具有不同的属性和DataDecorator的适当变化。Contact变化更大的地方在于其对contactType属性使用了EnumeratorDecorator。为了支持这一点,我们首先在头文件中定义一个枚举器,它包含我们想要的所有可能的值:

enum eContactType {
    Unknown = 0,
    Telephone,
    Email,
    Fax
};

请注意,我们有一个由0表示的默认值Unknown。这很重要,因为它允许我们容纳一个初始未设置的值。接下来,我们定义一个映射器容器,允许我们将每个枚举类型映射到一个描述性字符串:

std::map<int, QString> Contact::contactTypeMapper = std::map<int, QString> {
    { Contact::eContactType::Unknown, "" }
    , { Contact::eContactType::Telephone, "Telephone" }
    , { Contact::eContactType::Email, "Email" }
    , { Contact::eContactType::Fax, "Fax" }
};

当创建新的EnumeratorDecorator时,我们提供默认值(0 代表eContactType::Unknown)以及映射器:

contactType = static_cast<EnumeratorDecorator*>(addDataItem(new EnumeratorDecorator(this, "contactType", "Contact Type", 0, contactTypeMapper)));

我们的客户端模型稍微复杂一点,因为它不仅有数据项,还有子实体和集合。然而,我们创造和揭露这些东西的方式与我们已经看到的非常相似。

client.h:

#ifndef CLIENT_H
#define CLIENT_H

#include <QObject>
#include <QtQml/QQmlListProperty>

#include <cm-lib_global.h>
#include <data/string-decorator.h>
#include <data/entity.h>
#include <data/entity-collection.h>
#include <models/address.h>
#include <models/appointment.h>
#include <models/contact.h>

namespace cm {
namespace models {

class CMLIBSHARED_EXPORT Client : public data::Entity
{
    Q_OBJECT
    Q_PROPERTY( cm::data::StringDecorator* ui_reference MEMBER 
                                           reference CONSTANT )
    Q_PROPERTY( cm::data::StringDecorator* ui_name MEMBER name CONSTANT )
    Q_PROPERTY( cm::models::Address* ui_supplyAddress MEMBER 
                                     supplyAddress CONSTANT )
    Q_PROPERTY( cm::models::Address* ui_billingAddress MEMBER 
                                     billingAddress CONSTANT )
    Q_PROPERTY( QQmlListProperty<Appointment> ui_appointments READ 
                        ui_appointments NOTIFY appointmentsChanged )
    Q_PROPERTY( QQmlListProperty<Contact> ui_contacts READ ui_contacts 
                                          NOTIFY contactsChanged )

public:    
    explicit Client(QObject* parent = nullptr);
    Client(QObject* parent, const QJsonObject& json);

    data::StringDecorator* reference{nullptr};
    data::StringDecorator* name{nullptr};
    Address* supplyAddress{nullptr};
    Address* billingAddress{nullptr};
    data::EntityCollection<Appointment>* appointments{nullptr};
    data::EntityCollection<Contact>* contacts{nullptr};

    QQmlListProperty<cm::models::Appointment> ui_appointments();
    QQmlListProperty<cm::models::Contact> ui_contacts();

signals:
    void appointmentsChanged();
    void contactsChanged();
};

}}

#endif

我们将子实体公开为指向派生类型的指针,将集合公开为指向模板化EntityCollection的指针。

client.cpp:

#include "client.h"

using namespace cm::data;

namespace cm {
namespace models {

Client::Client(QObject* parent)
    : Entity(parent, "client")
{
    reference = static_cast<StringDecorator*>(addDataItem(new 
                StringDecorator(this, "reference", "Client Ref")));
    name = static_cast<StringDecorator*>(addDataItem(new 
                StringDecorator(this, "name", "Name")));
    supplyAddress = static_cast<Address*>(addChild(new Address(this), 
                                          "supplyAddress"));
    billingAddress = static_cast<Address*>(addChild(new Address(this), 
                                          "billingAddress"));
    appointments = static_cast<EntityCollection<Appointment>*>
    (addChildCollection(new EntityCollection<Appointment>(this, 
                                            "appointments")));
    contacts = static_cast<EntityCollection<Contact>*>(addChildCollection(new EntityCollection<Contact>(this, "contacts")));
}

Client::Client(QObject* parent, const QJsonObject& json)
    : Client(parent)
{
    update(json);
}

QQmlListProperty<Appointment> Client::ui_appointments()
{
    return QQmlListProperty<Appointment>(this, appointments->derivedEntities());
}

QQmlListProperty<Contact> Client::ui_contacts()
{
    return QQmlListProperty<Contact>(this, contacts->derivedEntities());
}

}}

添加子实体遵循与数据项相同的模式,但使用addChild()方法。请注意,我们添加了多个相同地址类型的子代,但确保它们具有不同的key值,以避免重复和无效的 JSON。实体集合是用addChildCollection()添加的,除了模板化,它们遵循相同的方法。

虽然创建我们的实体和数据项需要大量的工作,但是创建模型真的非常简单,现在它们都包含了我们本来不会拥有的特性。

在用户界面中使用我们花哨的新模型之前,我们需要在cm-ui中的main.cpp中注册类型,包括表示数据项的数据装饰器。记得先补充一下相关的#include语句:

qmlRegisterType<cm::data::DateTimeDecorator>("CM", 1, 0, "DateTimeDecorator");
qmlRegisterType<cm::data::EnumeratorDecorator>("CM", 1, 0, "EnumeratorDecorator");
qmlRegisterType<cm::data::IntDecorator>("CM", 1, 0, "IntDecorator");
qmlRegisterType<cm::data::StringDecorator>("CM", 1, 0, "StringDecorator");

qmlRegisterType<cm::models::Address>("CM", 1, 0, "Address");
qmlRegisterType<cm::models::Appointment>("CM", 1, 0, "Appointment");
qmlRegisterType<cm::models::Client>("CM", 1, 0, "Client");
qmlRegisterType<cm::models::Contact>("CM", 1, 0, "Contact");

完成后,我们将在MasterController中创建一个客户端实例,我们将使用它来填充新客户端的数据。这与我们用于添加其他控制器的模式完全相同。

首先,将成员变量添加到MasterController的私有实现中:

Client* newClient{nullptr};

然后,在Implementation构造函数中初始化它:

newClient = new Client(masterController);

第三,添加访问器方法:

Client* MasterController::newClient()
{
    return implementation->newClient;
}

最后添加Q_PROPERTY:

Q_PROPERTY( cm::models::Client* ui_newClient READ newClient CONSTANT )

我们现在有一个空的客户端实例可供用户界面使用,特别是CreateClientView,接下来我们将编辑它。首先为新的客户端实例添加快捷方式属性:

property Client newClient: masterController.ui_newClient

请记住,属性都应该在根项目级别定义,并且您需要import CM 1.0来访问注册的类型。这只是让我们能够使用newClient作为访问实例的简写,而不是每次都必须键入masterController.ui_newClient

此时,一切都准备就绪,您应该能够运行应用并毫无问题地导航到新的客户端视图。视图还没有对新的客户端实例做任何事情,但是它很高兴地坐在那里准备采取行动。现在,让我们看看如何与它互动。

自定义文本框

我们将从客户的name数据项开始。当我们在用户界面中使用另一个QString属性处理欢迎消息时,我们用基本的文本组件显示它。该组件是只读的,因此要查看和编辑我们的属性,我们需要获取其他东西。基地QtQuick模块有两个选项:TextInputTextEditTextInput用于单行可编辑纯文本,而TextEdit处理多行文本块,也支持富文本。TextInput是我们名字的理想选择。

Importing the QtQuick.Controls module makes additional text-based components like Label, TextField, and TextArea available. Label inherits and extends Text, TextField inherits and extends TextInput and TextArea inherits and extends TextEdit. The basic controls are enough for us at this stage, but be aware that these alternatives exist. If you find yourself trying to do something with one of the basic controls which it doesn’t seem to support, then import QtQuick.Controls and take a look at its more powerful cousin. It may well have the functionality you are looking for.

让我们在所学的基础上,创建一个新的可重用组件。像往常一样,我们将从准备我们需要的样式属性开始:

readonly property real sizeScreenMargin: 20
readonly property color colourDataControlsBackground: "#ffffff"
readonly property color colourDataControlsFont: "#131313" 
readonly property int pixelSizeDataControls: 18 
readonly property real widthDataControls: 400 
readonly property real heightDataControls: 40

接下来,在cm/cm-ui/components中创建StringEditorSingleLine.qml。这不是最美的名字,但至少是描述性的!

It's generally helpful to use a prefix with custom QML views and components to help distinguish them from the built-in Qt components and avoid naming conflicts. If we were using that approach with this project, we could have called this component CMTextBox or something equally short and simple. Use whatever approach and conventions work for you, it makes no functional difference.

编辑components.qrcqmldir,就像我们之前做的那样,使新组件在组件模块中可用。

我们试图通过该组件实现以下目标:

  • 能够从任何数据模型传入任何StringDecorator属性并查看/编辑该值
  • 查看在StringDecoratorui_label属性中定义的控件描述性标签
  • 查看/编辑TextBoxStringDecoratorui_value属性
  • 如果窗口足够宽,则标签和文本框水平布局
  • 如果窗口不够宽,则标签和文本框垂直布局

牢记这些目标,执行StringEditorSingleLine,如下所示:

import QtQuick 2.9
import CM 1.0
import assets 1.0

Item {
    property StringDecorator stringDecorator

    height: width > textLabel.width + textValue.width ? 
    Style.heightDataControls : Style.heightDataControls * 2

    Flow {
        anchors.fill: parent

        Rectangle {
            width: Style.widthDataControls
            height: Style.heightDataControls
            color: Style.colourBackground
            Text {
                id: textLabel
                anchors {
                    fill: parent
                    margins: Style.heightDataControls / 4
                }
                text: stringDecorator.ui_label
                color: Style.colourDataControlsFont
                font.pixelSize: Style.pixelSizeDataControls
                verticalAlignment: Qt.AlignVCenter
            }
        }

        Rectangle {
            id: background
            width: Style.widthDataControls
            height: Style.heightDataControls
            color: Style.colourDataControlsBackground
            border {
                width: 1
                color: Style.colourDataControlsFont
            }
            TextInput {
                id: textValue
                anchors {
                    fill: parent
                    margins: Style.heightDataControls / 4
                }
                text: stringDecorator.ui_value
                color: Style.colourDataControlsFont
                font.pixelSize: Style.pixelSizeDataControls
                verticalAlignment: Qt.AlignVCenter
            }
        }

        Binding {
            target: stringDecorator
            property: "ui_value"
            value: textValue.text
        }
    }
}

我们从一个公共的StringDecorator属性开始(公共的,因为它在根 Item 元素中),我们可以从组件外部设置它。

我们引入了一种新的元素——流——来为我们布局标签和文本框。“流”项将并排布局其子元素,直到用完可用空间,然后将它们像页面上的单词一样换行,而不是总是像行或列那样沿单个方向布局内容。我们通过将它锚定到根项目来告诉它有多少可用空间可以使用。

接下来是文本控件中的描述性标签和TextInput控件中的可编辑值。我们将这两个控件嵌入到显式大小的矩形中。矩形帮助我们对齐元素,并给我们机会绘制背景和边框。

Binding组件在两个不同对象的属性之间建立依赖关系;在我们的例子中,TextInput控件称为textValue,而StringDecorator实例称为stringDecoratortarget属性定义了我们要更新的对象,property是我们要设置的Q_PROPERTY,而value是我们要设置的值。这是给我们真正双向绑定的关键元素。没有这个,我们就可以从StringDecorator查看数值,但是我们在 UI 中所做的任何更改都不会更新数值。

回到CreateClientView,用我们的新组件替换旧的文本元素,并传入ui_name属性:

StringEditorSingleLine {
    stringDecorator: newClient.ui_name
}

现在构建并运行应用,导航到“创建客户端”视图,并尝试编辑名称:

如果您切换到“查找客户端”视图并再次返回,您将看到该值被保留,这表明在字符串装饰器中成功地设置了更新。

我们新绑定的视图还没有完全充满数据,但是在接下来的章节中,我们将向这个视图添加越来越多的内容,所以让我们添加一些收尾工作来为我们做准备。

首先,我们只需要向视图中添加另外三四个属性,由于我们为窗口设置的默认大小非常小,我们将耗尽空间,因此在MasterView中,将窗口大小增加到适合您显示的大小。我自己请客,1920 x 1080 全高清。

即使有一个更大的窗口可以使用,我们仍然需要为溢出的可能性做好准备,所以我们将把我们的内容添加到另一个名为ScrollView的新元素中。顾名思义,它以类似的方式工作,根据可用空间流动和管理内容。如果内容超出可用空间,它将为用户显示滚动条。这也是一个非常手指友好的控件,在触摸屏上,用户只需拖动内容,而不必摆弄小小的滚动条。

虽然我们目前只有一个属性,但是当我们添加更多属性时,我们将需要布局它们,因此我们将添加一个列。

最后,控件被固定在视图的边界上,所以我们将在视图周围添加一点檐槽,并在列中添加一些间距。

修改后的视图应该如下所示:

import QtQuick 2.9
import QtQuick.Controls 2.2
import CM 1.0
import assets 1.0
import components 1.0

Item {
    property Client newClient: masterController.ui_newClient

    Rectangle {
        anchors.fill: parent
        color: Style.colourBackground
    }

    ScrollView {
        id: scrollView
        anchors {
            left: parent.left
            right: parent.right
            top: parent.top
            bottom: commandBar. top
            margins: Style.sizeScreenMargin
        }
        clip: true
        Column {
            spacing: Style.sizeScreenMargin
            width: scrollView.width
            StringEditorSingleLine {
                stringDecorator: newClient.ui_name
                anchors {
                    left: parent.left
                    right: parent.right
                }
            }
        }
    }

    CommandBar {
        id: commandBar
        commandList: masterController.ui_commandController.ui_createClientViewContextCommands
    }
}

构建并运行,您应该会看到漂亮整洁的屏幕边距。您还应该能够将窗口的大小从宽调整到窄,并看到字符串编辑器会相应地自动调整其布局。

摘要

这是一个相当重要的章节,但是我们已经讨论了任何业务线应用中最重要的元素,那就是数据。我们已经实现了一个自我感知实体的框架,这些实体可以将自己序列化到 JSON 和从 JSON 序列化,并开始构建数据绑定控件。我们已经设计并创建了我们的数据模型,现在正进入回家阶段。在第 6 章单元测试中,我们将向我们迄今为止被忽略的单元测试项目展示一些爱心,并检查我们的实体是否按预期运行。