一. 合计行不参与排序
datagridview是采用datatable绑定datasource的情况下,最后一行是合计行,最后一行不参与排序而一直在最下面一行。主要是要实现CellMouseClick和Sorted方法,点击了Header后先保存最后一行合计行的内容,并Remove该行,然后按照系默认的方式排序,Sorted方法在排序后执行,把最后一行再添加到datagridview的末尾。

对于另外一种非绑定datatable的,而手动print出来的datagridview,可以采用重写Sort_compare方法,使得最后一行不参与排序,具体可以参看这里,注:此种方式发现的问题有两个。1.如果表格内容较多,一行行的将数据打印出来会使得列表刷新反应较慢。2.比较列的元素需要实现IComparable,特别是如果该列中有空或者NULL的时候,系统可能会不知道如何NULL与其他元素比较大小,会报错。

        private List<object[]> lastRow = new List<object[]>();
        private int colindex = 0;
        private string lastSortColName = null;
        private void dataGridView1_CellMouseClick(object sender, DataGridViewCellMouseEventArgs e)
        {
            if (e.RowIndex >= 0 || this.dataGridView1.Rows.Count == 0)
                return;

            if (lastRow.Count == 0)
            {
                colindex = e.ColumnIndex;
                int index = this.dataGridView1.Rows.Count - 1;
                lastRow.Add(((DataTable)this.dataGridView1.DataSource).Rows[index].ItemArray);

                this.dataGridView1.Rows.Remove(this.dataGridView1.Rows[this.dataGridView1.Rows.Count - 1]);
            }
        }

        private void dataGridView1_Sorted(object sender, EventArgs e)
        {
            if (lastRow.Count == 0)
                return;

            DataTable dt = ((DataTable)this.dataGridView1.DataSource);
            DataView dv = dt.DefaultView;
            dv.Sort = dt.Columns[colindex].ColumnName;
            if(this.lastSortColName == null || this.lastSortColName != dv.Sort)
            {
                lastSortColName = dv.Sort;
                dv.Sort = dv.Sort + " ASC";
            }
            else
            {
                dv.Sort = dv.Sort + " DESC";
                lastSortColName = null;
            }
            dt = dv.ToTable();
            dt.Rows.Add(lastRow[0]);
            lastRow.Clear();
            this.dataGridView1.DataSource = dt;

        }

注: datatable的DefaultView中Sort的 columnName + “ASC/DESC”的排序依据和执行sql中构建order by的结果不一致(我的是mysql utf-8),就是说datagridview采用的默认元素比较策略和sql的不一致,尤其是你自己实现了Sort_Compare的情况下(指定datasource模式该事件不会触发)。因此在刷新datagridview数据的时候如果要保持之前的排序规则,不用利用sql的order,而设置结果的datatable的DefaultView的Sort,并table = toTable().

二. 表格最左侧添加行号

在每行左边的可点击区域添加行号,并在最后一行添加合计。

        private void dataGridView1_RowPostPaint(object sender, DataGridViewRowPostPaintEventArgs e)
        {
            try
            {
                SolidBrush v_SolidBrush = new SolidBrush(dataGridView1.RowHeadersDefaultCellStyle.ForeColor);
                int v_LineNo = 0;
                v_LineNo = e.RowIndex + 1;

                int x = 15;
                int y = 5;
                string v_Line = v_LineNo.ToString();
                if (e.RowIndex == this.dataGridView1.Rows.Count - 1)     //最后一行row
                {
                    v_Line = "合计";
                    x = 5;
                }
                e.Graphics.DrawString(v_Line, e.InheritedRowStyle.Font, v_SolidBrush, e.RowBounds.Location.X + x, e.RowBounds.Location.Y + y);
            }
            catch (Exception ex)
            {
                MessageBox.Show(ex.Message);
            }
        }

三. 打印时候的问题

c#的打印实际上是自己把需要打印的数据利用事件,把内容画在一块画布上,每次打印一页都会调用打印文档PrintDocument象所绑定的PrintPage函数。很多C#的打印控件在设置完打印属性后会自动先生成打印预览,然后点击预览界面上的打印如果现打印结果和预览结果不一致,特别是有一堆随机大小的空白的话,需要确认所绑定的printPage函数没有使用什么全局的变或者至少每次调用的时候变量都被正确的初始化,因为打印预览是会调用绑定的printPage函数,然后点打印实际上还是会用,重绘一遍printDocument发送给打印机,而不是把预览的printPreviewDialog中的Document直接发送去打印,因此可能在生成打印预览和生成真正向打印机传递绘制的printDocument时某些变量,宽度之类的数据不一致而导致跟预览结果不一致相当郁闷,折腾了好几天。结果就是没有在绑定的PrintPage函数中每次先把纵坐标设置为最上面。

最近忙的要疯,毕业焦虑中,扯淡的毕业论文和更加扯淡的实验,在毕业的巨大压力下,抽空给别人在写一个合同管理软件,与不懂软件,计算机使用不熟悉的用户而言,什么才是一款合适的软件,使得他们工作更加方便,留几个Tips。

Stupid:所有的设计和描述都通俗易懂,一定要用不熟悉计算机的用户也可以简单理解的文字来描述软件的功能,例如Button上的Text,描述的Label等,不要让用户进行思考就可以明白这个是干什么的,因此思考就有可能带来理解上的错误。

Lazy: 很多电脑不熟悉打字速度并不快,特别是一些上一辈的人很多人都不会打字,因此在输入的时候,尽量把所有的输入选项做成一种下拉列表框,记忆所有的输入过的数据(可以按照出现频度排序),方便新数据的录入。

Convenience: 把可能需要用到的修改功能放在使用它的地方。例如用户输入表单的时候,该内容需要进行编辑(例如一个固定的下拉列表选项),用户发现选项中不存在需要的内容或者有内容输入错误,可以直接在这里进行添加和修改等操作,而不用进入到系统配置之类的菜单中。

Familiar: 对于年龄较大的用户而言,他这么多年所形成的习惯远比计算机上面的东西来的熟悉和了解,因此在开发软件时,围绕原来该功能的原型系统进行设计,使得用户在使用软件时的操作习惯与之前在普通介质上一致。

Comfortable: 你会觉得一个Button上写“上页”和“上 页”对用户产生的感受不同么,而且用户会要明确指出此类问题。

Alternative: 所有的东西都使可以用户选择,也许很多时候他们熟悉的名称和计算机专业采用的名称标识并不一样,要给用户修改的权力,也可以让用户有权力根据自己的喜好来设计界面。一个新对话框的初始位置是它上次关闭的位置或许就是一个很好的主意。

曾经在T公司的时候,有关界面设计上的培训,有三条原则。

  • 操作前可预知:用户操作之前知道此操作会带来的结果。
  • 操作中有反馈:操作执行中使得用户了解当前操作的进度和状态。
  • 操作后可撤销:操作后可以撤销操作返回。

一句话,软件就是服务业,而且,众口难调。

paxos算法是为了解决在分布式系统中,多个process有关某一个值达成一致决议的算法,即一种一致性算法(Consensus Algorithm)。Google的分布式锁Chubby和Yahoo的Zookeeper的实现中都采用了此算法思想,Lamport(此人的牛逼程度可以参看他发表的文章和被引用的次数)在《paxos made simple》的第二页给出的abstract就一句话:“The Paxos algorithm, when presented in plain English, is very simple.”,有关paxos发布的背后的事情还挺有趣的,paxos从提出到发布用了9年,<The Part-Time Parliament>,Lamport虚拟了一个希腊的城邦paxos,用该城邦的人如何就一条法令达到一致来描述了整个算法,但计算机界的N多人都不悲剧幽默感,Lamport就在2001年写了一篇相对简短的《paxox made simple》,估计也就是这句abstract的来由

paxos算法将分布式系统的process划分为三种角色,分别为proposer, acceptor以及learner,其中只有proposer和acceptor参加决议过程,learner只是了解决议被批准以后系统具体选择的决议值,整个算法采用基于消息的传递模型,假设整个系统是消息异步,并且没有拜占庭失效问题(non-Byzantine model),就是整个系统中的消息可以延迟到达,可以重复发送甚至可以丢失,但不能被篡改,允许系统中的节点失效和重启,但需要这些节点在失效到重启之间具有永久性存储介质用以记录信息。

整个paxos算法的过程是一个两段式提交,由proposer提出决议(value),acceptor接受并选择决议,用一个{n,v}来对传递的消息进行描述,n表示一个严格递增不重复的编号,有关这个编号的实现在paxos made simple中提到的是让所有的proposer都从不相交的数据集合中进行选择,例如系统有5个proposer,则可以为每一个proposer分配一个标识j(0~4),则每一个proposer每次提出决议的编号可以为5*i + j(i可以用来表示round),v表示value。

Phase 1a: proposer提出一个决议并选择一个编号N,发送message prepare(N)给大多数(Majority)的Acceptor。

Phase 1b: acceptor接受到prepare(N)的消息后,如果不违法自己给其他proposer的承诺,即没有收到过比N编号更高的prepare请求,则返回给proposer消息{n,v},n是自己上次批准的请求编号,v是自己上次accept的value。并承诺自己不再接受任何小于N的编号的prepare请求。如果acceptor之前已经收到过其他高于N的编号的prepare请求,则忽略prepare(N),也可以给一个deny反馈(出于效率考虑,不反馈不影响正确性)。

Phase 2a: proposer收到多数的acceptor的确认反馈后,即可以进入批准阶段,proposer选择一个value(V)(V所有acceptor回馈的消息中,编号最大的value),或者当任何acceptor都没有value反馈时,proposer可以自己任意选择value值,发送Accept(N,V)给所有的acceptor。

Phase 2b: 当acceptor接受到一个accept(N,V)的请求时,acceptor就批准这个请求,除非该acceptor之前收到了一个比编号N更高的prepare请求。

整个算法的执行过程中,proposer可以任何时间随意的抛弃一个proposal,不影响正确性。算法的执行图解如下(from wikipedia)

 Client   Proposer      Acceptor     Learner
   |         |          |  |  |       |  |
   X-------->|          |  |  |       |  |  Request
   |         X--------->|->|->|       |  |  Prepare(N)
   |         |<---------X--X--X       |  |  Promise(N,{Va,Vb,Vc})
   |         X--------->|->|->|       |  |  Accept!(N,Vn)
   |         |<---------X--X--X------>|->|  Accepted(N,Vn)
   |<---------------------------------X--X  Response
   |         |          |  |  |       |  |

Learner需要了解被选择了的value,从上图可以看出在Phase 2b阶段时,当acceptor接受了一个决议后,会给Learner发送消息通知learner,为了尽快的使得Learner了解到被选择的value,可以每一个Acceptor都发送消息给每一个Learner,这样会产生大量的消息,是Acceptor个数和Learner的乘积。另外一种是由于系统中没有拜占庭失效,可以采用一个主Learner接受Acceptor发送的决议并且通知其他的Learner,这样减少了消息总量但会引起单点失效和多一级的传递。因此可以在实践中做一个权衡,选一个主learner集合来对learner进行通知。

由于有可能存在活锁的情况,即两个proposer轮流的用更高的编号提出新的prepare请求,使得没有任何一方的决议能成功的被批准。因此会选择一个主proposer,只有该proposer才能提出决议,如果主proposer提出一个决议遇到了deny,即存在另外一个更高的编号决议,则直接选择一个足够大的编号进行提议。主proposer,即leader包括主learn的选择一般也采用paxos算法,zookeeper中leader的选择就采用了fast paxos算法。

虽然Lamport说paxos算法非常simple,然后文章也很简短,但跟所有学习者的感觉一样,远没有大师的智慧,不会觉得他特别simple,看了几天也还只了解大概,特别是在看到相关引申的各种文章后。有关paxos算法在实现角度的描述以及实现可以参考Lamport在2005年写的<Fast Paxos>,以及他人写的<Paxos Made Live – An Engineering Perspective>,和<Paxos在大型系统中常见的应用场景>。

Reference:

[1] http://en.wikipedia.org/wiki/Paxos_algorithm (对整个paxos系列描述的相当详细)

[2] http://zh.wikipedia.org/wiki/Paxos算法 (paxos made simple的简单译文)

[3] Paxos Made Simple

[4] Paxos Made Live – An Engineering Perspective