65.9K
CodeProject 正在变化。 阅读更多。
Home

Ruby on Rails 实现的 PropertyGrid

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.79/5 (4投票s)

2013 年 11 月 20 日

CPOL

6分钟阅读

viewsIcon

20406

downloadIcon

179

使用 JQuery UI 和最少的 Javascript 创建一个动态属性网格编辑器,可以以流畅的编程风格或最小的 DSL 进行初始化。

从 GitHub 获取源代码

git clone https://github.com/cliftonm/property_grid_demo 

此控件的代码现在可以作为 gem 安装

gem install property_grid 

并可从此处下载

git clone https://github.com/cliftonm/property_grid

引言

我需要一个通用的属性网格编辑器,它支持一些高级功能,如日期/时间选择器、颜色选择器等,基于仅在运行时才知道的记录字段(这最终是我“Spider UI”文章系列下一部分的内容)。  这里有一个时髦的基于 Javascript 的属性网格 这里,但我想要一些 Javascript 最少,并且更“Ruby-on-Rails”的东西。  我还想要一个可以与记录字段类型很好地集成的服务器端控件,它会根据表字段等模式信息动态生成网格。

我构建了一组类来方便在服务器端构建属性网格控件的内容。  您会注意到我选择了实际的类和“流畅”的编程风格,但如果您不喜欢实际实现使用“流畅”技术的方式,我还构建了一个非常小的内部领域特定语言 (DSL),您可以使用它代替——基本上只是隐藏(使用静态数据)构建属性网格实例的内部管理的调用方法。

与我之前的文章一样,我将使用 Sass 和 Slim 脚本来编写 CSS 和 HTML 标记。

支持类

有几个支持类

  • PropertyGrid - 组和组属性的容器
  • Group - 属性组
  • GroupProperty - 组内的属性

类 PropertyGrid

# A PropertyGrid container
# A property grid consists of property groups.
class PropertyGrid
  attr_accessor :groups

  def initialize
    @groups = []
  end

  # Give a group name, creates a group.
  def add_group(name)
    group = Group.new
    group.name = name
    @groups << group
    yield(group)         # yields to block creating group properties
    self                 # returns the PropertyGrid instance
  end
end

此类有两个要点

  1. 因为 add_group 执行 yield(group),所以调用者可以提供一个块来添加组属性。 
  2. 因为 add_group 返回 self,所以调用者可以继续以流畅的编程风格添加更多组。

因此,我们可以编写这样的代码

@property_grid = PropertyGrid.new().
  add_group('Text Input') do |group|
    # add group properties here.
  end.  #<----  note this syntax
  add_group('Date and Time Pickers') do |group|
    # add group properties here.
  end

注意“点”:end. - 因为 add_group 在 yield 之后返回 self,所以我们可以使用流畅的编程风格继续添加组。

类 Group

# Defines a PropertyGrid group
# A group has a name and a collection of properties.
class PropertyGroup
  attr_accessor :name
  attr_accessor :properties

  def initialize
    @name = nil
    @properties = []
  end

  def add_property(var, name, property_type = :string, collection = nil)
    group_property = GroupProperty.new(var, name, property_type, collection)
    @properties << group_property
    self
  end
end

一个组有一个名称并管理一个属性集合。 add_property 类返回 self,所以我们再次可以使用流畅的表示法

group.add_property(:prop_c, 'Date', :date).
  add_property(:prop_d, 'Time', :time).
  add_property(:prop_e, 'Date/Time', :datetime)

注意每次调用 add_property 后的“点”,允许我们再次调用 add_property,操作同一个组实例。

这并不能阻止我们使用更符合 Ruby 习惯的语法,例如

group.properties <<
  GroupProperty.new(:prop_c, 'Date', :date) << 
  GroupProperty.new(:prop_d, 'Time', :time) <<
  GroupProperty.new(:prop_e, "Date/Time", :datetime)

类 GroupProperty

此类是实际属性的容器

include PropertyGridHelpers

class GroupProperty
  attr_accessor :property_var
  attr_accessor :property_name
  attr_accessor :property_type
  attr_accessor :property_collection

  # some of these use jquery: https://jqueryui.jqueryjs.cn/
  def initialize(var, name, property_type, collection = nil)
    @property_var = var
    @property_name = name
    @property_type = property_type
    @property_collection = collection
  end

  def get_input_control
    form_type = get_property_type_map[@property_type]
    raise "Property '#{@property_type}' is not mapped to an input control" if form_type.nil?
    erb = get_erb(form_type)

    erb
  end
end

我稍后会讨论 get_erb 的作用。

请注意,需要三个字段

  1. 模型属性的符号名称
  2. 属性的显示文本
  3. 属性类型

可选地,可以传入一个集合,它支持下拉控件。  集合可以是简单的数组

['Apples', 'Oranges', 'Pears']

或“记录”,实现 idname 属性,例如

# A demo of using id and name in a combo box
class ARecord
  attr_accessor :id
  attr_accessor :name

  def initialize(id, name)
    @id = id;
    @name = name
  end
end

@records =
[
  ARecord.new(1, 'California'),
  ARecord.new(2, 'New York'),
  ARecord.new(3, 'Rhode Island'),
]

这适用于 ActiveRecord 对象集合。

类 ControlType

此类是渲染 Web 控件所需信息的容器

class ControlType
  attr_accessor :type_name
  attr_accessor :class_name

  def initialize(type_name, class_name = nil)
    @type_name = type_name
    @class_name = class_name
  end
end

这非常基础——它只是类型名称和一个可选的类名。  目前,类名仅用于 jQuery 控件。

定义属性类型

属性类型定义在 property_grid_helpers.rb 中——这是一个简单的函数,返回一个 type => ControlType 的哈希数组。

def get_property_type_map
  {
    string: ControlType.new('text_field'),
    text: ControlType.new('text_area'),
    boolean: ControlType.new('check_box'),
    password: ControlType.new('password_field'),
    date: ControlType.new('datepicker'),
    datetime: ControlType.new('text_field', 'jq_dateTimePicker'),
    time: ControlType.new('text_field', 'jq_timePicker'),
    color: ControlType.new('text_field', 'jq_colorPicker'),
    list: ControlType.new('select'),
    db_list: ControlType.new('select')
  }
end

您可以在此处扩展或更改类型映射到 Web 查询的规范。  显然,您不限于使用 jQuery 控件。

DSL 实现会是什么样子?

让我们看看如果我将属性网格容器对象写成 DSL 会是什么样子。  如果您有兴趣,这里有一篇关于在 Ruby 中编写内部 DSL 的精彩教程 这里,我正在做的事情应该看起来非常相似。  基本上,DSL 使用 Builder 模式,如果您对 Ruby 中的设计模式感兴趣,这里是一个很好的教程。

我们想要的是能够声明一个属性网格实例,就好像它是 Ruby 语言的一部分一样。  所以我将从

@property_grid = new_property_grid
group 'Text Input'
group_property 'Text', :prop_a
group_property 'Password', :prop_b, :password
group 'Date and Time Pickers'
group_property 'Date', :prop_c, :date
group_property 'Time', :prop_d, :date
group_property 'Date/Time', :prop_e, :datetime
group 'State'
group_property 'Boolean', :prop_f, :boolean
group 'Miscellaneous'
group_property 'Color', :prop_g, :color
group 'Lists'
group_property 'Basic List', :prop_h, :list, ['Apples', 'Oranges', 'Pears']
group_property 'ID - Name List', :prop_i, :db_list, @records

实现包含三个方法

  1. new_property_grid 
  2. group 
  3. 属性

它们基本上是用于构建属性组及其属性实例的工厂模式。  实现位于一个模块中,并利用我们底层的类

module PropertyGridDsl
  def new_property_grid(name = nil)
    @__property_grid = PropertyGrid.new

    @__property_grid
  end

  def group(name)
    group = Group.new
    group.name = name
    @__property_grid.groups << group

    group
  end

  def group_property(name, var, type = :string, collection = nil)
    group_property = GroupProperty.new(var, name, type, collection)
    @__property_grid.groups.last.properties << group_property

    group_property
  end
end

此实现利用了 @__property_grid 变量,该变量维护当前在 DSL 脚本中应用的实例。  我们不使用单例模式,因为我们希望允许在同一网页上拥有多个属性网格实例。

优点显而易见——生成的属性网格生成脚本紧凑且易于阅读。  上面的 DSL 很简单——它基本上只是包装了处理底层类细节的辅助方法。

正如 Martin Fowler 在 这里所写,虽然内部 DSL 经常会增加“语法噪音”,但一个写得好的 DSL 实际上应该减少“语法噪音”,就像这个简单的 DSL 所做的那样。  例如,比较 DSL

@property_grid = new_property_grid
group 'Text Input'
group_property 'Text', :prop_a 

与非 DSL 实现: 

@property_grid = PropertyGrid.new().
  add_group('Text Input') do |group|
    group.add_property(:prop_a, 'Text').
    add_property(:prop_b, 'Password', :password)
  end 

当然,使用类实现,即使是“流畅”的形式,也比 DSL 更嘈杂!

整合

您需要一个视图、一个控制器和一个模型来将所有这些组合在一起。

视图 

基本视图很简单。  给定模型,我们实例化一个列表控件,其中每个列表项本身都是一个具有两列和一行的表格

=fields_for @property_grid_record do |f|
  .property_grid
    ul
      - @property_grid.groups.each_with_index do |group, index|
        li.expanded class="expandableGroup#{index}" = group.name
        .property_group
          div class="property_group#{index}"
            table
              tr
                th Property
                th Value
              - group.properties.each do |prop|
                tr
                  td
                    = prop.property_name
                  td.last
                    - # must be processed here so that ERB has the context (the 'self') of the HTML pre-processor.
                    = render inline: ERB.new(prop.get_input_control).result(binding)

  = javascript_tag @javascript
  
  javascript:
    $(".jq_dateTimePicker").datetimepicker({dateFormat: 'mm/dd/yy', timeFormat: 'hh:mm tt'});
    $(".jq_timePicker").timepicker({timeFormat: "hh:mm tt"});
    $(".jq_colorPicker").minicolors()

我将不展示驱动此结构视觉呈现的 CSS。

Javascript

请注意,有两个 Javascript 部分。  一个是直接编码在表单中以支持 jQuery dateTimePickertimePickercolorPicker 控件。

另一个 Javascript 是以编程方式生成的,因为它控制属性组是折叠还是展开,这需要为每个属性组提供唯一的处理程序。  由于这仅在运行时知道,因此 Javascript 由此函数(在 property_grid_helpers.rb 中)生成

def get_javascript_for_group(index)
  js = %Q|
    $(".expandableGroup[idx]").click(function()
    {
      var hidden = $(".property_group[idx]").is(":hidden"); // get the value BEFORE making the slideToggle call.
      $(".property_group[idx]").slideToggle('slow');

      // At this point, $(".property_group0").is(":hidden");
      // ALWAYS RETURNS FALSE

      if (!hidden) // Remember, this is state that the div WAS in.
      {
        $(".expandableGroup[idx]").removeClass('expanded');
        $(".expandableGroup[idx]").addClass('collapsed');
      }
      else
      {
        $(".expandableGroup[idx]").removeClass('collapsed');
        $(".expandableGroup[idx]").addClass('expanded');
      }
    });
  |.gsub('[idx]', index.to_s)

  js
end 

ERB 

请注意上面这一行

= render inline: ERB.new(prop.get_input_control).result(binding)

这会处理 ERB 代码,该代码也是以编程方式生成的,因为我们需要一个特定于属性类型的控件。  这是由我们之前看到的 get_erb 函数生成的。

# Returns the erb for a given form type. This code handles the construction of the web control that will display
# the content of a property in the property grid.
# The web page must utilize a field_for ... |f| for this construction to work.
def get_erb(form_type)
  erb = "<%= f.#{form_type.type_name} :#{@property_var}"
  erb << ", class: '#{form_type.class_name}'" if form_type.class_name.present?
  erb << ", #{@property_collection}" if @property_collection.present? && @property_type == :list
  erb << ", options_from_collection_for_select(f.object.records, :id, :name, f.object.#{@property_var})" if @property_collection.present? && @property_type == :db_list
  erb << "%>"

  erb
end 

模型

我们需要一个模型来存储我们的属性值。  在演示中,模型位于 property_grid_record.rb

class PropertyGridRecord < NonPersistedActiveRecord
  attr_accessor :prop_a
  attr_accessor :prop_b
  attr_accessor :prop_c
  attr_accessor :prop_d
  attr_accessor :prop_e
  attr_accessor :prop_f
  attr_accessor :prop_g
  attr_accessor :prop_h
  attr_accessor :prop_i
  attr_accessor :records

  def initialize
    @records =
      [
        ARecord.new(1, 'California'),
        ARecord.new(2, 'New York'),
        ARecord.new(3, 'Rhode Island'),
      ]

    @prop_a = 'Hello World'
    @prop_b = 'Password!'
    @prop_c = '08/19/1962'
    @prop_d = '12:32 pm'
    @prop_e = '08/19/1962 12:32 pm'
    @prop_f = true
    @prop_g = '#ff0000'
    @prop_h = 'Pears'
    @prop_i = 2
  end
end

这只是初始化我们的测试数据。

控制器

控制器将所有内容组合在一起,实例化模型,指定属性网格属性和类型,并获取以编程方式生成的 Javascript

include PropertyGridDsl
include PropertyGridHelpers

class DemoPageController < ApplicationController
  def index
    initialize_attributes
  end

  private

  def initialize_attributes
    @property_grid_record = PropertyGridRecord.new
    @property_grid = define_property_grid
    @javascript = generate_javascript_for_property_groups(@property_grid)
  end

  def define_property_grid
    grid = new_property_grid
    group 'Text Input'
    group_property 'Text', :prop_a
    group_property 'Password', :prop_b, :password
    group 'Date and Time Pickers'
    group_property 'Date', :prop_c, :date
    group_property 'Time', :prop_d, :date
    group_property 'Date/Time', :prop_e, :datetime
    group 'State'
    group_property 'Boolean', :prop_f, :boolean
    group 'Miscellaneous'
    group_property 'Color', :prop_g, :color
    group 'Lists'
    group_property 'Basic List', :prop_h, :list, ['Apples', 'Oranges', 'Pears']
    group_property 'ID - Name List', :prop_i, :db_list, @property_grid_record.records

    grid
  end
end

还有一个支持函数(在 property_grid_helpers.rb 中)

def generate_javascript_for_property_groups(grid)
  javascript = ''

  grid.groups.each_with_index do |grp, index|
    javascript << get_javascript_for_group(index)
  end

  javascript
end

结论

像这样的东西应该很容易移植到 C# / ASP.NET,我很乐意听任何这样做的人的反馈。  否则,请尽情享用,并告诉我您是如何改进这个概念的。 

© . All rights reserved.