[SEO]SEO Google算法解析系列之潜在语义索引(LSI)

mikel阅读(601)

  作为一个SEOer,我们必须对搜索引擎排名算法有一定的理解,才能真正谈优化,接下来将针对Google一系列算法就我的一点理解和心得与大家共享,希望大家多多指教,相互学习。这里我们先从关键词的相关性算法开始。

   搜索引擎作弊最快的方法当属关键词堆砌,这源于信息检索中相关性算法本身的缺陷,为了对抗这种作弊方法,搜索引擎通过潜在语义索引 (Latent Semantic Indexing,LSI)算法来发现这些作弊页面,LSI算法也是信息检索领域一种古老的算法,1988年由S.T. Dumais等 人提出,主要用于自然语言理解,通过统计的方法对文档的进行语义分析,发掘同义词,相关词组等等。举个简单的例子:比方“汽车消费”这个词,通过分析大量 页面发现这个词频繁的出现在“汽车消费贷款”,“中国汽车消费网”等等这些词组中,那么机器可以认为人们的语言习惯是将“汽车消费”和“汽车消费贷款”、 “中国汽车消费网”等等联系在一起来描述一些事情。通过这样的分析发现一些由机器生成的关键词堆砌页面,因为搜索引擎认为机器生成的页面不会出现这些相关 联的词组。

  LSI算法被用于 Google的很多应用,如Adwords,Google Suggest,以及上面提到的反作弊等等。

  LSI算法提醒我们在搜索引擎优化的时候要注意页面的关键词密度,以及相关词组的使用,尽量使用比较自然的语言方式来提高页面的相关性。

原文发在:http://www.admin5.com/article/20090904/178038.shtml

[MVC]MVC+JQuery开发B/S系统:②表单绑定

mikel阅读(583)

标题冠名MVC其实跟MVC没多大关系了。。 目前只是写的前台,请求的都是后台给的Json数据。

逻辑比较复杂的Form绑定起来比较麻烦,这些都是要自己写代码。而简单的我们可以写一个通用的进行处理。不需要反复的 xxx.Text = "xxx" ..

MVC有自己的自动映射功能,我们这里用JQuery来遍历Controls进行绑定。
如果用过asp开发过系统的人都知道以前取表单的值都是request.form("controlName"),用到的是name而不是id。
所以我们的表单在制作的时候元素的Name值不能没有。 为了能够写通用的方法,我们约定所有的元素的name 是 "cName" 格式 ,"c"+"字段名",id自己随便。
由于Js的Dictionary区分大小写,所以我们这些名字也对大小写敏感,包括上一节的列表绑定都是这样。

$.fn.bindForm = function(model) {
    
if (model == undefined || model == null) {
        
return;
    }
    
var formId = this.attr("id");
    $(
"input,textarea,select""#" + formId).each(function() {
        
var cname = $(this).attr("name");
        
var cid = $(this).attr("id");
        
if (cname == "")
            
return;
        
if (cid == "") {
            cid 
= $(this)[0].tagName + "[name='" + cname + "']";
            $(
this).attr("id", cname);
        } 
else
            cid 
= "#" + cid;
        $(cid).bindControl(model[cname.replace(
"c""")], formId);
    });
    
return this;
 

$.fn.bindControl = function(value, formId) {
    
if (value == undefined)
        
return this;
    value 
= value.toString();
    formId 
= formId || "";
    
if (formId != "")
        formId 
= "#" + formId + " ";
    
switch (this.attr("type")) {
        
case "select-one"//DropDownList
            //this[0].selectedIndex = 0;
            //$("option[value='" + value + "']", this).attr("selected");
            var isSelected = false;
            
this.children().each(function() {
                
if (this.value == value) {
                    
this.selected = true;
                    isSelected 
= true;
                    
return;
                }
            });
            
if (!isSelected)
                
this[0].selectedIndex = 0;
            
break;
        
case "select-multiple"//ListBox
            this.children().each(function() {
                
var arr = value.split(',');
                
for (var i = 0; i < arr.length; i++) {
                    
if (this.value == arr[i]) {
                        
this.selected = true;
                    }
                }
            });
            
break;
        
case "checkbox"//CheckboxList
            //单选
            if (value.indexOf(','== 1) {
                $(formId 
+ "input[name='" + this.attr("name"+ "']").each(function() {
                    
if (this.value == value) {
                        $(
this).attr("checked"true);
                        
return;
                    }
                });
            }
            
//多选
            else if (this.attr("type"== 'checkbox') {
                
var arr = value.split(',');
                
for (var i = 0; i < arr.length; i++) {
                    $(formId 
+ "input[name='" + this.attr("name"+ "']").each(function() {
                        
if (this.value == arr[i]) {
                            
this.checked = true;
                        }
                    });
                }
            }
            
break;
        
case "radio"//RadioButtonList
            $(formId + " input[name='" + this.attr("name"+ "']").each(function() {
                
if (this.value == value) {
                    
this.checked = true;
                    
return;
                }
            });
            
break;
        
default//Normal
            this.val(value);
            
break;
    }
    
return this;
}
绑定表单就显得比较容易了。 
$("#form1").bindForm(<%=Json(ViewData["model"])%>); 简单的一句话,就自动把值绑定了。
绑定一个控件也很容易 $("#controlId").bindControl(value); 
其实在实际开发中,我们会经常碰到 级联的DropDownList , 这样在绑定的时候 我们还要对具体的Control 执行绑定,并且trigger他的event。 这个叫双向绑定,目前还没做成自动化。

[JavaScript]javascript的拖放(第2部分)

mikel阅读(590)

实现手柄拖动的功能,就是把mousedown的事件侦听器放到handle中。由于我们原先程序的骨架搭建得比较好,添加新功能非常容易。

1.#
2.  (handle || el).onmousedown = dragstart;

但这不够人性化,最好是我们输入一个类名作为handle的参数,拖动类会自动根据此类名在其子孙元素寻找此元素。

01.if(handle){
02.  var cls = new RegExp("(^|\\s)" + handle + "(\\s|$)");
03.  for(var i=0,l=el.childNodes.length;i <l;i++){
04.    var child = el.childNodes[i];
05.    if(child.nodeType == 1 && cls.test(child.className)){
06.      _handle = child;
07.      break;
08.    }
09.  }
10.}

相应地方修改为:

1.#
2.  (_handle || el).onmousedown = dragstart;

有时如果拖动元素是个非常复杂,包含相当多东西,这样拖起来很吃内存,这时是不是拖动一个空元素是不是好一些呢?我们可以克隆原来的拖动元素,并把 它加入原来元素的后面(相同z-Index,后面的会放到前面的前面)。如果要求是用手柄拖动,我们把原来的手柄也克隆过来就是。

01.if(ghosting){ // ghosting为可选参数,表示使用克隆体拖动
02.  _ghost = el.cloneNode(false);
03.  el.parentNode.insertBefore(_ghost,el.nextSibling);
04.  if(_handle){
05.    _handle = _handle.cloneNode(false);
06.    _ghost.appendChild(_handle);
07.  }
08.    !+"\v1"? _ghost.style.filter = "alpha(opacity=50)" : _ghost.style.opacity = 0.5;
09.}

我们还可以增添一些回调函数(onStart,onDrag,onEnd),如果用户没有实现回调函数,我们给个空函数(function(){})它就好了。

像拖动时显示元素的坐标,我们也可以做成可选参数coords。默认为true,并在手柄上显示,但这样会覆盖住原来的一些东西,我们在拖动前把它保存到一个变量中,拖动后再还原。

另,如果是一个范围中拖动,如一个指定的容器或浏览器的可视区,当拖动元素存在margin时,其右边与下边可能会超出容器。因此我们必须取出其相 应的margin,当右边与下边的坐标大于容器右边或下边的坐标时,强逼元素往左边与上边移动相应的值,那个值不用说就是margin值。我们用一个简单 的getStyle()函数来取margin值,因此设置元素的margin时请以px为单位,否则在IE中会计算错误。

01.var getStyle = function(el, style){
02.  if(!+"\v1"){
03.    style = style.replace(/\-(\w)/g, function(all, letter){
04.      return letter.toUpperCase();
05.    });
06.    var value = el.currentStyle[style];
07.    (value == "auto")&&(value = "0px" );
08.    return value;
09.  }else{
10.    return document.defaultView.getComputedStyle(el, null).getPropertyValue(style)
11.  }
12.}

最后一个较为重要的功能,当元素往右边或下边移动,让元素永远留在可视区。这怎样做到呢?我们计算元素右下角的坐标(right与bottom), 然后再计算出浏览器可视区的宽与高,当right大于宽时,就让它们的差值作为浏览器的scrollLeft,bottom的情形相仿。

01.if(scroll){ //表示让滚动条与元素一起移动
02.  var doc = isQuirk ? document.body : document.documentElement;
03.  doc = options.container || doc;
04.  var a = getCoords(el).left + el.offsetWidth;
05.  var b = doc.clientWidth;
06.  if (a > b){
07.    doc.scrollLeft += a - b;
08.  }
09.  var c = getCoords(el).top + el.offsetHeight;
10.  var d = doc.clientHeight;
11.  if (c > d){
12.    doc.scrollTop += c - d;
13.  }
14.}

最后函数扩展成以下样子,功能比较多,下附文档说明:

参数 类型 说明
id string 必选,拖动元素的ID
container object 可拖的范围,必须为拖动元素的父级元素,是否为定位元素无所谓。
limit boolean 默认false,与container配合使用。当值为true时,它会以container或浏览器的可视区作拖动范围。
lockX boolean 默认false。当值为true时,锁定X轴,只允许上下移动。
lockY boolean 默认false。当值为true时,锁定Y轴,只允许左右移动。
handle string 手柄的类名,当设置了此参数后,只允许用手柄拖动元素。
ghosting boolean 默认false。当值为true时,会生成一个与拖动元素相仿的影子元素,拖动时只拖动影子元素,以减轻内存消耗。
revert boolean 默认false。当值为true时,让拖动元素在拖动后回到原来的位置。
coords boolean 默认true。拖动时在手柄或影子元素上显示元素的坐标。
scroll boolean 默认false。当值为true时,允许滚动条随拖动元素移动。
onStart function 在拖动前鼠标按下的那一瞬执行。
onDrag function 在拖动时执行。
onEnd function 在拖动后鼠标弹起的那一瞬执行。

[MVC]Oxite分析之Skinning

mikel阅读(622)

change set:42944

download :http://oxite.codeplex.com/SourceControl/ListDownloadableCommits.aspx

Oxite 中可以非常方便的切换皮肤(Skinning),并且前台皮肤(Site Skin)和后台皮肤(Admin Skin)都可以切换。Oxite的皮肤包括三部分:CSS + images, scripts and views。注意这个views就是MVC中的View部分,这样就带来更大的灵活性。这里提一下,Skin的设置会影响到ViewEngine在什么位 置搜索View文件。官方也有关于Skinning的介绍,不太详细:http://oxite.codeplex.com/Wiki/View.aspx?title=Skinning
一、设置Skin

在Oxite管理后台的站点设置里可以设置前台和后台皮肤。进入后台点击"Settings"菜单下的"Site"子菜单,进入"Edit Site"页面:

222 
在Skins Path字段中设置存放各个Skin的总目录,默认值是/Skins。注意这个路径是虽然是绝对路径,但它是站点所在虚拟目录的根目录下的Skins目录,也就是说/Skins实际上对应的是~/Skins。
假设HomeController下的Index这个Action的View的搜索,
Skins Path 字段值不为空时并且Site Skin(或Admin Skin)为空时,将会搜索~/Skins/Views/Home/和~/Skins/Views/Shard/目录;如果搜索不到,将会搜索~ /Views/Home/和~/Views/Shard/目录。
注意:目前Oxite版本尚未支持Scripts Path和Styles Path字段,也就是说你的设置将暂时不会有效果。我通过源码分析的,尚未测试设置过。
Site Skin是用来设置具体的Skin的,默认为空。这种情况下,可以回头看看我们分析Skins Path字段时所说的。我们如果直接把根目录的Styles、Scripts、Views这三个目录移到Skins目录下,站点将一切正常。
如 果我们为Site Skin设定一个值的话,比如MySkin。在不移动官方源码的情况下,站点不会有任何变化;我们如果直接把根目录的Styles、Scripts、 Views这三个目录移到Skins目录下,站点将仍然正常;如果我们在Skins目录下建立一个MySkin目录,把根目录的Styles、 Scripts、Views这三个目录移到该目录下,站点将仍然仍然正常。为什么呢?原因是Site Skin设定一个值后,ViewEngine搜索Views时候,只是多了两个搜索路径。比如HomeController下的Index这个 Action,将会首先搜索:~/Skins/MySkin/Views/Home/和~/Skins/MySkin/Views/Shard/,如果搜 索不到,将会搜索~/Skins/Views/Home/和~/Skins/Views/Shard/目录;如果还是搜索不到,将会搜索~/Views /Home/和~/Views/Shard/目录。
另外,如果Skins Path 字段设置为空,Site Skin字段设置为MySkin。在这种情况下,View搜索的路径是这样的:
~/MySkin/Views/Home/ –> ~/MySkin/Views/Shard/ –> ~/Views/Home/ –> ~/Views/Shard/
Skins Path 字段和Site Skin字段是可以设置为更复杂的路径,我觉得完全没必要。参考Oxite默认的配置,Skins目录下放置各种Skins已经足够用了。
二、Oxite如何实现Skinning

待续…

[MVC]MVC+Jquery开发B/S系统:①列表绑定

mikel阅读(594)

OK,今天看到首页一篇MVC+JS的文章。最近我在用这两样东西。 发一下自己在应用中的应用方法。

我们想,WebForm绑定列表有repeater和Gridview ,如果用MVC我们还用控件当然不是不行,就是有点说不过去了吧?(啥子说不过去?)
控件既然如此方便。我们就用Js来实现简单的控件绑定。 这里都是用到的JQuery。如果有人觉得不爽,请立即停止阅读。

请思考下面三个问题: Ⅰ如何表达一个控件?ItemTemplate如何表示?

Ⅱ有了模板如何执行替换?
Ⅲ如何得到模板需要的DataSource?
先解决第一个问题,我们定好模板的规则才方便写替换方法。也才能和Controller开发人员约定DataSource的格式。
Ⅰ如何表达一个控件?ItemTemplate如何表示?
其实很简单,我们不必做十分复杂的列表。就算做我们也要留个后路才处理复杂的情况。
看一下HTML:

<table width="100%" border="0" cellspacing="0" cellpadding="0" class="list" id="lstAdvertiser">
    
<thead>
        
<tr>
            
<th style="width: 80px">
                
<href="JavaScript:void(0)" sort="AdvertiserId">ID</a>
            
</th>
            
<th>
                
<href="JavaScript:void(0)" sort="AdvertiserName">广告客户</a>
            
</th>
            
<th style="width: 120px">
                品牌管理
            
</th>
            
<th style="width: 120px">
                操作
            
</th>
        
</tr>
        
<!–
        <tr id="item_{AdvertiserId}">
            <td>
                {AdvertiserId}
            </td>
            <td style="text-align: left;">
                {AdvertiserName}
            </td>
            <td>
                <a href="/Brand/Add?AccountId={AdvertiserId}" target="_new" title="创建品牌">
                    创建</a> <a href="/Brand/List?AccountId= {AdvertiserId}&AdvertiserName= {AdvertiserName}" target="_new" title="品牌管理">
                        查看</a>
            </td>
            <td>
                <a href="/Advertiser/Edit?Id={AdvertiserId}" target="_new" title="修改广告客户">修改</a>
                <a href="/Advertiser/Delete?id={AdvertiserId}" target="_delete">删除</a>
            </td>
        </tr>
    
–>
    
</thead>
    
<tbody>
    
</tbody>
    
<tfoot>
    
</tfoot>
</table>
我把ItemTemplate放在thead里是为了绑定后,tbody被填充而把注释也“冲掉”,这样翻页的时候就找不到ItemTemplate了。
可能有同学说不如放在XML里来管理,或者用一个特殊的标签来放模板,其属性target来指定该table,或者…。 我都十分赞同,我也想过很多,目前采用的注释,可是注释有个大缺点就是IE在处理HTML时会把注释处理的乱七八糟-_-。
好的,ItemTemplate十分简单,下面我们要替换这个Item也是轻而易举的事情。
Ⅱ有了模板如何执行替换?请看Js

//==========================================================================
//
替换模板得到数据
//
==========================================================================
function replaceTemplate(template, data) {
    
if (data == undefined || data.length == 0)
        
return;
    
if (template == undefined)
        
return;
    
var resultHtml = "";
    
for (var i = 0; i < data.length; i++) {
        
var rowHtml = template;
        
if (i % 2 == 1)
            rowHtml 
= rowHtml.replace("<tr""<tr class=\"bg\"");
        
var re = /\{(\w+)\}/gi;
        
if (data[i] == undefined) {
            alert(i);
            alert(data.length);
            
break;
        }
        
while ((fields = re.exec(template)) != null) {
            
var re1 = new RegExp("\{" + fields[1+ "\}""gi");
            rowHtml 
= rowHtml.replace(re1, data[i][fields[1]]);
        }
        resultHtml 
+= rowHtml;
    }
    
return resultHtml;
}
//==========================================================================
//
获取指定ID的模板 IE下对个别控件无效(非容器一般无效)
//
==========================================================================
function getTemplate(id) {
    
var t = $("#" + id).html();
    
if (t == null)
        
return "";
    t 
= t.match(/<!–([\s\S]*?)–>/);
    
if (t != null)
        t 
= t[1];
    
return t;
遍历DataSource,替换Item,然后把HTML添加到tbody里。 just only so so …
Ⅲ如何得到模板需要的DataSource?
很明显,我们的替换方法里DataSource是JS的Array。 我们知道.net提供了Json序列化的方法。 我们可以把List序列化为Js的Array,把对象序列化为一条Json数据(其实Json也是Array,只不过是Dictionary形式)
MVC框架说实话我用的不多,但是我十分喜欢这个开发模式,这样可以让前台和后台彻底的分离, 开发起来十分愉快。
我们知道一个Action可以返回void或JsonResult, 这便是我们需要的。
Controller开发人员从Model取得数据后,把他序列化后Response或Return Json(data); 这样前台直接请求该Action便得到的是Json数据。

/// <summary>
/// 获取JSON格式的List
/// </summary>
/// <returns></returns>
public JsonResult GetListJson()
{
    Response.Cache.SetNoStore();
    
int RecordCount;
    var list 
= GetList(out RecordCount);
    
return Json(new { recordCount = RecordCount, list });
}
上面是一个取得Json的Action。 GetList是一个已有方法。
————————————————————————————————–
其实列表的绑定比较复杂,涉及到排序、翻页、复选框、绑定后的行列处理、嵌套绑定。 这需要其他的方法来实现,请阅读

[Flash]Adobe Flash Catalyst 最佳实践

mikel阅读(507)

 这是一篇来自Adobe开发者中心的文章,原文链接:http://www.adobe.com/devnet/flashcatalyst/articles/flash_catalyst_best_practices.html

Adobe Flash Catalyst 是个新的专业交互设计工具,使用它,您可以不用写代码,快速的创建应用程序接口,和交互内容。

通常,我们使用Flash Catalyst时,你会觉得工作在整个开发团队的边缘,并且很难将你的Flash Catalyst项目文件与团队的其他成员分享,以便于他们可以继续开发这个项目,或者使用其中你定义的的组件皮肤。

越接近你使用Illustrator设计和构建的艺术原创稿,你的FC项目就越具备冲击力,随之而来的,这种冲击力将会延续到资源和皮肤的质量和有效性上,使得开发者使用这些资源变为可能。

在这篇文章中,我将会提供一些示意和提示以便于你可以更容易地结构化地将设计的资源从Illustrator里移动到 Flash Catalyst里。 我同样会强调一些Flash Catalyst的最佳实践,这将会确保你可以用最少的重构时间来让你的资源可以继续在Flash Builder里开发。

使用Illustrator

注意:这篇文章主要集中在使用Adobe Illustrator做艺术设计之后为Flash Catalyst使用上。 你也可以使用Adobe Photoshop或者Adobe Fireworks来做设计和创作;下面很多提示和说明在这些工具中同样是适用的,虽然我不准备在这里介绍他们的使用方法。

设计好你的Illustrator文件结构

你可以整理好你的Illustrator文件结构,这样可以使应用程序中不同的功能模块区域和用户界面被定义在分开的层和子层中(见图1)。

图1 Illustrator 中的层面板(左)和Flash Catalyst中的层面板(右)。

你可以通过在你在Illustrator中指定逻辑结构和有意义的名字,使在Illustrator 中定义的层,子层,和组在Flash Catalyst中同样有效;在使用Flash Catalyst工作的时候你将会明白这个文件结构的用途。 这很重要,你需要在转换设计稿到组件时选定层和组,以及程序中不同功能区间之间的转换动画。

在单个画板上进行设计

Flash Catalyst不支持多画板设计,所以,比起在不同的画板上表现不同的功能区间和用户界面,使用一个单独的画板,但将不同的区间分布到不同的层里面,会比较好(见图2)。

图2 在Illustrator项目中的层面板定义了一个程序中的五个功能区间,例如背景层和前景层。

在图2中,背景图和前景在程序中保持一致(并且总是显示),每一个其他的顶级层表现了用户可以看到的不同的程序功能区间。

为每个部分都命名

就像给层和组命名一样,为Illustrator 文件中的每个独立图形和元素命名也是很重要的。 你也许会考虑在整个文件中使用共用命名策略,比如用“Button_name”来指定这些图形元件将会在随后转换成元件时,被转换成按 钮,“Text_name”“TextInput_name”“Custom_name”,以此类推。

元素的命名并非一个正式的规范,但是使用命名策略会得到整个互动开发项目中的设计师和工程师的认可,这样会让所有人都知道如何在程序中使用这些组件,从而避免之后可能发生的混乱和返工。

不要链接外部的图片文件

在团队环境中,你很可能需要将你的Illustrator文件交付给一个使用Flash Catalyst工作的互动设计师;不要在Illustrator中链接外部文件,取而代之的是将它们置于Illustrator文件内部,这样你可以通 过单个的文件做转换,并确保随后的设计师能在项目的随后工作中使用全部的资源。 Illustrator 中的图片和其他资源将会显示在Flash Catalyst中的Library面板中。

在你将图片导入进Illustrator之后,你要确定你执行了Object > Rasterize 选项来改善图片的显示效果。 在这个对话框中,为界面浏览选择72 DPI 、typically,你需要将背景设置为透明。

标记副本组件和资源的

在Illustrator中通常需要提取出多态的,重复的按钮,输入文本框,和其他的可视元素,以便于描绘整个程序界面的全景图形设计。 在Flash Catalyst中,你需要为每个美术设计定义一个单独的组件,但是,当是同样的设计的时候,就要避免定义多个组件。

比起来依赖于不重复的元件来做判断,不如考虑在Illustrator文件里为元件标记一个副本,这样你就可以很清楚的知道他们是不需要被重新定义的。

你可以考虑使用一个命名策略,例如用主设计上用“TextInput_Blue”标记这个组件,用“_textInput_Blue”来表示一个副 本。 或者你也可以选择将所有副本组件的alpha值设置为40%(见图3);这从视觉上表示这个组件不需要被重定义,你可以在Flash Catalyst里用一个库中的实例来置换它。

图3 上方的输入文本框定义了一个在Flash Catalyst中的组件。

使用“create outlines”来为文本添加滤镜和效果

虽然,Flash Catalyst在支持Illustrator文件格式方面做的很出色,但是你会发现,文本的滤镜和效果并没有像你预期的那样被渲染出来。如图4所示。

图4. 原始设计(左)和在导入到Flash Catalyst之后的效果(右)。

在这个例子里,你可以在Illustrator 使用“create outlines”选项解决这个问题,并为文本创建一个矢量的表现。 不过,这意味着,这个文本将变成不可编辑的了。

你可以将这种方法使用在那些你不准备绑定到你程序中的,较小数量的静态文本上,例如,标题,商标等等。

不要在Illustrator中定义所有组件区域

虽然你可以在Illustrator中定义所有的组件,但这没有什么意义,因为Flash Catalyst中同样提供了比较的全面的选项用于编辑文本和图形元件的属性,包括填充,渐变,和描边(见图5).

图5 即将面世版本的Flash Catalyst中的属性面板。

你会因此发现在Illustrator 设计原稿和随后都可以很方便的做调整,一旦你将一个组件导入到Flash Catalyst中,就可以使用states面板来设置调整每一个区域的可见属性。

为数据列表组件定义一个单独的元素

当将原稿转换为Flash Catalyst中的数据列表组件时,你只需要指定一个单独的重复使用的列表选项。 如果你需要在Illustrator的用户界面中绘制出数据列表的全貌,你可以选择定义两种截然不同的区域:使用在Flash Catalyst中的独立版本,和一个包含了其余项的分散的组。(见图6)

图6. 为数据列表设计的填充项,下面的组表现出这个列表在Illustrator中看起来的样子。

这种方法可以利用组来排列和分隔这些选项在数据列表中的样子,但是最后在随后的开发中将他们移除。

Flash Catalyst

下面的一些点,是讲如何使用Flash Catalyst和你的团队一起工作了。

在基本规范上使用“Save as copy”

当你在Flash Catalyst中使用不同的方式进行过实验,并学习如何更好的使用这个新工具,你会发现使用“Save as copy”选项对频繁的使用是很有价值的。 在着手构建你的主项目结构时,这显得尤为重要,例如对组和层次做重大调整时,或者添加新的功能区间和定义过渡方式时。

如果有什么东西是不能在计划中完全确定的,你可以很快捷的恢复到之前这个文档保存的版本,然后尝试其他方式。

按照正确的顺序进行工作

当你把一个静态的设计原稿从Illustrator文件中导入进一个Flash Catalyst中的动态的富媒体网络应用时,按照正确的逻辑顺序开始工作就变得非常重要了。 你会喜欢这种方式,并在你以往的工作经验基础上适应它的。

  1. 将所有设计原稿中的东西转换为组件。(使用层面板中的 显示/隐藏 功能,显示程序在某个特定时间程序界面应该显示的东西。)
  2. 创建一个自定义的,或者通用的组件,来定义一个程序的功能小模块(例如,为程序界面的表单创建一个自定义的组件,或者一个独立的窗口)
  3. 图7 这个表单在 Flash Catalyst中将被转化成一个自定义,或者通用的组件。

  4. 这个组在功能区域里面下的可见元件被引入到逻辑集合中;这在定义场景过渡时会显得特别有用,你可以成组的设置元素的属性,而不必一个个的去单独设置。(见图8)
  5. 图8. 成组的元素,使得通过时间线来操作他们变得更加容易。

  6. 使用自定义的组件和定义在组件上交互效果来创建过渡动画,将会带来组件区间的变化。
  7. 在某个时间点创建一个状态,对于每一个状态要注意以下几点:
    1. 定义组件的位置,可见性,和出现效果。
    2. 使用时间线来指定新的状态和之前的状态之间的过渡。
    3. 在一个状态下定义元件的交互行为,这些行为将会触发程序状态的切换

使用优化过的图形

从Illustrator中导入过于复杂的矢量图形是一种不必要的开销,而且在Flash Player里渲染这些图形也会造成程序的性能问题。 如果在你的程序界面中,有这样的图形,并不需要运行时在其上做任何相应的改变,那么你可以在Flash Catalyst中使用转换为优化图形选项(见图9)。

图9 在复杂的矢量图形上使用转换为优化图形选项。

如果你想要矢量图形在被导入进Flash Catalyst中时自动被优化转换,你应该在Illustrator的原稿中将其定义为符号。

考虑组件的重用性

当在Flash Catalyst中做组件转换时,通常要为在Illustrator中定义的文本指定尺寸,例如按钮,单选框,和复选框。 如果你计划在别处重复使用这些组件,请确保在组件中延长文本框的尺寸以适应其他标签。(见图10)

图10 调整单选按钮中的文本以适应组件。

定义有意义的状态名称

你在 Flash Catalyst中定义的状态名称,同样被使用在输出的程序代码中,这对于前端的开发团队是相当有价值的,因为这个名字说明了它应该被定义成怎样的组件。(图11)

图11 在Flash Catalyst中使用 Pages/States 面板做状态命名。

在任何情况下,状态名称都应该清楚的诠释用户在程序中那一个功能点上需要处理的事情,正在浏览的页面,或者其他有意义的叙述。

在库中重命名组件

跟状态命名一样,在库中为组件命名是广泛应用在整个程序代码编写和项目构建过程中的。 考虑到与前端开发团队沟通的关系,指定统一的命名规范对从互动设计到开发的过渡很有帮助。(见图12)

图12 TextInputSm和TextInputMd显而易见的表示小码和中码的输入文本组件;而RepeatedItem1和RepeatedItem2表达的就不是那么的清楚了。

使用代码视图来做优化

不,你并不需要是一个资深的程序员,你只需要注意几件事情就可以了。

首先,你要鉴别所有文本框使用的字体都是标准的系统字体(例如Arial),或者是你已经绑定在程序中的字体。(这将会增加程序的体积) 如果你在一些地方使用了非标准的字体,例如,一个标志或一个标题,也许最好返回到Illustrator中把他们转换为图形。

其次,从Illustrator 中导入的资源,其冗长的代码段中包含精确的路径,渐变,描边,这些会使得资源更加生动,在Flash Catalyst里,这些也是可以被优化的。 检查包含代码的组的名称,然后在设计视图中,找到art board上的这个组,选择转化为优化图形。

记住,这个转变仅仅作用在单个状态下

当你在程序中和组件里做完了这个设计转化之后,如果你要你做的改动作用于所有状态,你需要通知Flash Catalyst,如果你不通知,那么这个改变将只会作用于当前的状态。 右键单击要改动的元素,选择“Make Same in All Other States”,在做完改变之后所有状态下的组件就都随之更改了(见图13);

 

 

图13 Make Same in All Other States选项确保任何修改都会被应用到所有的状态中。

在真实的浏览器环境中预览状态切换

虽然,你可以在Flash Catalyst中的时间轴上预览你定义的过渡动画,但将输出结果显示在参数真实的浏览器里无疑是最好的选择。(使用 File > Run Project 来做浏览)

从这里出发,我们还可以做什么

讨论Flash Catalyst和为Flash Catalyst的发展提出好的点子,请访问 Adobe Forums for Flash Catalyst 学习更多 Flash Catalyst知识,到Adobe Labs下载课件和教程。

获得公用许可。

这个工作是受到类似于3.0 Unported许可一样的非商用知识共享许可所认可的。

关于作者

Andrew Shorten是一个Adobe的平台技术传道士,并热衷于改良优化,丰富的基于电脑的用户体验。 Andrew在富士通,为政府,企业客户做过网站,报刊,以及移动设备的用户界面开发。 此后,他在Macromedia,Microsoft和Adobe工作,期间,他作为设计师,开发工程师,网站代理和组织帮助他们实现丰富的,动人和成功 的网站以及左面应用程序。

翻译:RIAMeeting翻译小组 王贺

[JavaScript]javascript的拖放入门

mikel阅读(635)

既然说是入门,本文给出的函数都尽可能短小,但不失强大,并兼容所有浏览器。要想拖动页面上的一个元素,说白了就是让它实现位移。CSS中最能体现 这种思想的是绝对定位,因为绝对定位能使元素脱离原来的文档流,但原来的物理空间还保留着,这就不影响周围的元素了。接着下来就是设置事件侦听器,分别在 mousedown,mousemove,mouseup绑定对应的回调函数,一旦触发这些事件,浏览器就会自动调用它们。我们分别把这些回调函数命名为 dragstart,drag与dragend。

  • 在dragstart方法中,我们的工作取得鼠标相对于事件源的距离。IE中,我们可以很轻松地用offsetX与offsetY实现,在 firefox中我们要用layerX与layerY。其他浏览器大多数是墙头草,两个都实现,但Opera是IE那一方的。为了实现全面兼容,就要绕点 远路,利用e.clientX – el.offsetLeft与e.clientY – el.offsetTop获取它们。并在此方法中开始监听mousemove与mouseup事件。
  • 在drag方法,我们要将拖动的距离加到原来的top与left上,以实现位移。拖动过程,可能引发沿文本的被选中,我们需要清除文本。
  • 在dragend方法,我们要卸载绑定事件,释放内存。

为了共享方法,我们把它们都做成原型方法。

01.var Drag = function(id){
02.  this.node = document.getElementById(id);
03.  this.node.style.position = "absolute"
04.  this.node.me = this;//保存自身的引用
05.  this.node.onmousedown = this.dragstart;//监听mousedown事件
06.}
07.Drag.prototype = {
08.  constructor:Drag,
09.  dragstart:function(e){
10.    var e = e || window.event,//获得事件对象
11.    self = this.me,//获得拖动对象
12.    node = self.node;//获得拖动元素
13.    //鼠标光标相对于事件源对象的坐标
14.    node.offset_x = e.clientX - node.offsetLeft;
15.    node.offset_y = e.clientY - node.offsetTop;
16.    node.onmousemove = self.drag;//监听mousemove事件
17.    node.onmouseup = self.dragend;//监听mouseup事件
18.  },
19.  drag:function(e){
20.    var e = e || window.event,//获得事件对象
21.    self = this.me,//获得拖动对象
22.    node = self.node;//获得拖动元素
23.    node.style.cursor = "pointer";
24.    //将拖动的距离加再在原先的left与top上,以实现位移
25.    !+"\v1"? document.selection.empty() : window.getSelection().removeAllRanges();
26.    node.style.left = e.clientX - node.offset_x  + "px";
27.    node.style.top = e.clientY - node.offset_y  + "px";
28.    node.onmouseup = self.dragend;//监听mouseup事件
29.  },
30.  dragend:function(){
31.    var self = this.me,//获得拖动对象
32.    node = self.node;//获得拖动元素
33.    node.onmousemove = null;
34.    node.onmouseup = null;
35.  }
36.}

现在我们的类就可以运作了,但正如你们所看到的那样,当鼠标拖动太快会出现鼠标移出div的情况。这是因为移动得越快,位移的距离就越大,拖动元素 一下子从我们的鼠标溜走了,就无法调用mouseup事件。在IE中我们可以利用setCapture()来补救,但一旦某个元素调用 setCapture(),文档中所有后续的鼠标事件都会在冒泡之前传到该元素,直到调用了releaseCapture()。换言之,在完成这些鼠标事 件之前,它是不执行其他事件,一直占着线程,于是出现了我们的光标离开拖动元素的上方也能拖动元素的怪异现象。

你在拖动块上点一下,然后再到拖动块外面点一下,就可以实现"隔空拖动"的神奇效果了!(当然只限IE)

由于鼠标事件一直接着线程,所以在我们不用的时候,一定要releaseCapture()来解放它。

在firefox中我们可以使用window.captureEvents(),火狐说这方法已经废弃,但我怎么在各标准浏览器中运作良好呢?! 不过不管怎么样,来来回回要设置捕获与取消捕获非常麻烦与吃内存,我们需要转换思路。因为如果鼠标离开拖动元素上方,我们的绑定函数就无法运作,要是把它 们绑定在document上呢,鼠标就无论在何处都能监听拖动元素。但触发拖动的onmousedown事件我们还保留在拖动元素上,这事件不会因为不执 行就引起差错之虞。不过,由于绑定对象一变,我们要在这些事件中获得拖动对象的引用的难度就陡然加大,这里我就直接把它们做成构造函数内的私有函数吧。

01.var Drag = function(id){
02.  var el = document.getElementById(id);
03.  el.style.position = "absolute";   
04.  var drag = function(e) {
05.    e = e || window.event;
06.    el.style.cursor = "pointer";
07.    !+"\v1"? document.selection.empty() : window.getSelection().removeAllRanges();
08.    el.style.left = e.clientX - el.offset_x  + "px";
09.    el.style.top = e.clientY - el.offset_y  + "px";
10.    el.innerHTML = parseInt(el.style.left,10)+ "X"+parseInt(el.style.top,10);
11.  }
12.  
13.  var dragend = function(){
14.    document.onmouseup = null;
15.    document.onmousemove = null;
16.  }
17.   
18.  var dragstart = function(e){
19.    e = e || window.event;
20.    el.offset_x = e.clientX - el.offsetLeft;
21.    el.offset_y = e.clientY - el.offsetTop;
22.    document.onmouseup = dragend;
23.    document.onmousemove = drag;
24.    return false;
25.  }
26.  el.onmousedown = dragstart;
27.}

进一步改进,不使用mouseup事件,这样就减少了错过mouseup事件带来的鼠标粘着卡壳的问题。但由于标准浏览器不会自动切换e.button和e.which的值,我被迫动用一个功能键Shirt来停止鼠标拖拽。

01.var Drag = function(id){
02.  var el = document.getElementById(id);
03.  el.style.position = "absolute";
04. 
05.  var drag = function(e){
06.    var e = e || window.event,
07.    button = e.button || e.which;
08.    if(button == 1 && e.shiftKey == false){
09.      el.style.cursor = "pointer";
10.        !+"\v1"? document.selection.empty() : window.getSelection().removeAllRanges();
11.      el.style.left = e.clientX - el.offset_x  + "px";
12.      el.style.top = e.clientY - el.offset_y  + "px";
13.      el.innerHTML = parseInt(el.style.left,10)+ " x "+parseInt(el.style.top,10);
14.    }else {
15.      document.onmousemove = null;
16.    }
17.  }
18. 
19.  var dragstart = function(e){
20.    e = e || window.event;
21.    el.offset_x = e.clientX - el.offsetLeft;
22.    el.offset_y = e.clientY - el.offsetTop;
23. 
24.    el.style.zIndex = ++Drag.z;
25.    document.onmousemove = drag;
26.    return false;
27.  }
28.  Drag.z = 999;
29.  el.onmousedown = dragstart;
30.}

虽然不绑定mouseup的确在IE爽到high起,但在火狐等浏览要多按一个键才能终止拖动,怎么说也对用户体验造成影响,因此当一种知识学学就算了。我们还是选择方案2。

接着下来我们为方案扩展一下功能。首先范围拖动,就是存在一个让它在上面拖动的容器。如果存在容器,我们就取得其容器的四个点的坐标,与拖动元素的 四个点的坐标比较,从而修正top与left。由于我已经实现了getCoords函数,取得页面上某一点的坐标易如反掌。同时这样做,我们就不用把此容 器设置成offsetParent。总之,多一事不如少一事。

01.var getCoords = function(el){
02.  var box = el.getBoundingClientRect(),
03.  doc = el.ownerDocument,
04.  body = doc.body,
05.  html = doc.documentElement,
06.  clientTop = html.clientTop || body.clientTop || 0,
07.  clientLeft = html.clientLeft || body.clientLeft || 0,
08.  top  = box.top  + (self.pageYOffset || html.scrollTop  ||  body.scrollTop ) - clientTop,
09.  left = box.left + (self.pageXOffset || html.scrollLeft ||  body.scrollLeft) - clientLeft
10.  return { 'top': top, 'left': left };
11.};

接着我们要取得容器的四个点的坐标:

1._cCoords = getCoords(container),
2._cLeft = _cCoords.left,
3._cTop = _cCoords.top,
4._cRight = _cLeft + container.clientWidth,
5._cBottom = _cTop + container.clientHeight;

接着是拖动元素的四个点的坐标:

1.var _left = el.offsetLeft,
2._top = el.offsetTop,
3._right = _left + el.offsetWidth,
4._bottom = _top + el.offsetHeight,

但是这样取得的是没有拖动前的坐标,拖动时的坐标在上面的方案2也已给出了:

1.var _left = e.clientX - el.offset_x,
2._top = e.clientY - el.offset_y,

修正坐标很简单,如果拖动元素在左边或上边超出容器,就让拖动元素的top与left取容器的_cLeft或_cTop值,如果从右边超出容器,我们用_cRight减去容器的宽度再赋给拖动元素的top,下边的处理相仿。

01.if(_left < _cLeft){
02.  _left = _cLeft
03.}
04.if(_top < _cTop){
05.  _top = _cTop
06.}
07.if(_right > _cRight){
08.  _left = _cRight - el.offsetWidth;
09.}
10.if(_bottom > _cBottom){
11.  _top = _cBottom - el.offsetHeight;
12.}
13.el.style.left = _left  + "px";
14.el.style.top = _top  + "px";

水平锁定与垂直锁直也是两个很常用的功能,我们也来实现它。原理很简单,在开始拖动时就把top与left保存起来,待到拖动时再赋给它就行了。代码请直接看运行框的代码:

Tag标签: javascript

[MVC]ASP.NET MVC数据验证

mikel阅读(586)

关于ASP.NET MVC的验证,用起来很特别,因为MS的封装,使人理解起来很费解。也可能很多人都在Scott Guthrie等人写的一本《ASP.NET MVC 1.0》书中,见过NerdDinner项目中对Dinner对象修改和添加的时的数据验证。但有许多封装的地方,不知道是怎样的工作原理,今天研究了,拿出来给大家分享一下。

数据库还是上一篇blog中的库与表,同样的方法来创建news表的实体类,在自动生成的news这个实体类中,我们发现有一个特殊的分部方法:

partial void OnValidate(System.Data.Linq.ChangeAction action);

这个方法没有实现,我们根据C#的语法知道,如果分部类中的分部方法,没有实现的话,调用和定议的地方都不会起什么作用。现在,我们要去完善这个方法,让它“用”起来。

首先,人产在Models中创建news类的另一部分,代码如下:


 1 public partial  class news
 2    {
 3        partial void OnValidate(System.Data.Linq.ChangeAction action)
 4        {
 5            if (!IsValid)
 6            {
 7                throw new ApplicationException("验证内容项出错!");
 8            }

 9        }

10        public bool IsValid
11        {
12            get return (GetRuleViolations().Count() == 0); }
13        }

14        public IEnumerable<RuleViolation> GetRuleViolations()
15        {
16            if (String.IsNullOrEmpty(this.title .Trim () ))
17                yield return new RuleViolation("题目步能为空!""题目");
18            if (String.IsNullOrEmpty(this.contents .Trim ()))
19                yield return new RuleViolation("内容不能为空!""内容");          
20            yield break;
21        }

22    }

23/// <summary>
24    /// 规则信息类
25    /// </summary>

26    public class RuleViolation
27    {
28        public string ErrorMessage getprivate set; }
29        public string PropertyName getprivate set; }
30
31        public RuleViolation(string errorMessage)
32        {
33            ErrorMessage = errorMessage;
34        }

35
36        public RuleViolation(string errorMessage, string propertyName)
37        {
38            ErrorMessage = errorMessage;
39            PropertyName = propertyName;
40        }

41    }

42

在这里给出这么多代码,其实是提前有设计的,因为从业务角度考虑,还不应该写这部分代码。RuleViolation类很简单,就是一个包括了两个属性的类(这个类的结构设计是根据后面的ModelState.AddModelError主法来设计的)。

news分部类中,有一个IsValid的属性,这个属性是bool类型的,返回值取决于GetRuleViolations这个方法,这个方法返回值是一个IEnumerable<RuleViolation>类型的,IEnumerable是通过news的几个属性是否为空来生成跌代的。如果titlecontentsNull””,就返回跌代。其实真正的用户数据的验证就是在这里实现,用户的数据的对与错,就是一个逻辑,只要用户数据不符合规则,就可以 “yield return new RuleViolation("错误标识","错误提示信息!");这里的错误码提示信息是显示到客户端的,所以要处理好友好的提示。

现在验证用户数据,生成错误列表的工作都做完了,但关键是怎么能让用户提交数据时,调用OnValidate。这个问题,先放一下,请记住,上面的代码,只要在用户提交数据时,调用OnValidate,这样就能得到错误集合。

现在,让我们来处理CotrollerView层,在Cotroller层,首先来添加index这个Action,代码如下:

1public ActionResult Index()
2        {           
3            var NewsList = DCDC.news.Select(newss=>newss);
4            return View(NewsList );
5     }

6

这个Action返回所有news表中的记录。

对应的View如下:

Code

代码中,需要我们注意是的    <%= Html.ActionLink("Edit", "Edit", new { id=item.ID }) %>

因为要导航到EditView,把以接下来我们创建EditActionView(因为在编辑数据时,要用到验证,Edit才是我们的重点)

1 public ActionResult Edit(int id)
2        {
3            var list = DCDC.news.Single(newss=>newss.ID ==id);
4            return View(list);
5     }

6

 

<%= Html.ActionLink("Edit", "Edit", new { id=item.ID }) %>中的id会被当成参数送到EditControllerEdit(int id)Action,成为Edit方法的实参。

Edit.aspx页面如下图:

对应EditAction生成view,代码如下:

 

Code

 

如果要单击“更新”返回数据新数据,还需要我们写如下一个Action

 

Code

 

这个EditAction是用户提交返来更新数据库的,我们可以从formValuews得到用户在页面上更新的数据,来更新Sig_news对象,然后调用DCDC.SubmitChanges();去更新数据库,如果没有民常,会导航到index.aspx页面。如果发生异常,就会运行到catch里。如果还记得,在本文的前半部分,我们说到OnValidate,是数据在提交时应该验证,但在这里,我们并没有显示的调用OnValidate这个方法,但实际运行中,我们发现,这个方法被执行了,如果我们建立跟踪,把断点设在DCDC.SubmitChanges();如果我们数据有民常,会发现当DCDC.SubmitChanges();执行完后就会跳到partial void OnValidate(System.Data.Linq.ChangeAction action)这个方法,这是怎么做到的呢?我们猜测,一定是在数据提交时,调用OnValidate这个方法。为了找到它们的关系,只好用Reflector.exe来“探测”一下了(Reflector.exe的用法就不说了)。

SubmitChanges方法是DataContext的一个方法,这个类位于System.Data.Linq命空间下,用Reflector.exe打开SubmitChanges,看到this.SubmitChanges(ConflictMode.FailOnFirstConflict);定位这个方法,可以看到new ChangeProcessor(this.services, this).SubmitChanges(failureMode);定位查找会发现ValidateAll(orderedList);在这个方法中,多处看到 SendOnValidate(obj2.Type, obj2, ChangeAction.Insert);这个方法,再定位,有这样一行代码 type.OnValidateMethod.Invoke(item.Current, new object[] { changeAction });这里,正是通过反射调用了OnValidate这个方法。这样我们就找到了SubmitChanges执行时调用OnValidate的方法了(其不用调用OnValidate也可以验证用户数据,只需要写个方法,在SubmitChanges 提交以前执行就可以达到同样效果)。同时,当发生异常时,OnValidate会抛出一个Application的异常,这里会被public ActionResult Edit(int id,FormCollection formValuews)方法中的Catch捕获到,就执行如下代码:

Code

这行代码的意思是把错误的信息,以键值的方式放入ModelState中,ModelState是一个ModelStateDictionary类型,这个类型实现了IDictionary<string, ModelState>, ICollection<KeyValuePair<string, ModelState>>, IEnumerable<KeyValuePair<string, ModelState>>, IEnumerable这些接口(这里要注意,ModelState是当前对象的一个属性,并且它的AddModelError方法的第一个参数key有其独特的作用)。处理完异常后,还是返回当前页面。这时你会发现,在页面的   <%= Html.ValidationSummary("Edit was unsuccessful. Please correct the errors and try again.") %>发生了变化,把我们错误的地方去提示出来了,这里就是,为什么我们把错误信息放到ModelState中,而错误则显示在了Html.ValidationSummary中了呢?并且发生错误的数据后会加上了一个红色的“*”,这是怎么样做到的呢?

再次利用Reflector.exe,查看Html.ValidationSummary方法和Html.ValidationMessage方法,会发现它们显示的数据是从ModelState 中获取的,如果ModelState 这个集合中没有数据,Html.ValidationSummaryHtml.ValidationMessage就返回空,如果发生异常,this.ModelState中有子项,就会通过Html.ValidationSummaryHtml.ValidationMessage在页面页上显示出来。因为Html.ValidationMessage在页面上有多个,所以在this.ModelState.AddModelError(v.PropertyName,v.ErrorMessage);方法中的v.PropertyName就有了用处了,这个值要与<%= Html.ValidationMessage("题目", "*")%>中的第一个参数对应,这样<%= Html.ValidationMessage("题目", "*")%>才能起到作用,显示出第二个参数“*”。

这样一来,就达到了ASP.NET MVC的数据验证。由于ASPNET MVC 验证捌的弯比较多,所以下来用个图来说明一下。

[UI]列表之美,简洁但不简单

mikel阅读(527)

我是一个Google fans,使用过Google产品的用户都应该能深刻体会到他的“简洁朴素”风格。美国一家统计公司曾经做过一项测试:用户如果在12秒钟内找不到自己所 需要的内容,就会超过忍耐限度,而选择离开。所以在Google 的“简洁朴素”风格背后隐藏着巨大的商业价值,这就是简洁的力量!

我们可以把列表页看作是一张信息页面的整合,如何将用户感兴趣的信息准确的传达给用户,如何让用户在这纷乱嘈杂的信息世界中找到自己所需要的,成为列表页改造问题中的一个重点。

列表之美,简洁但不简单

更简洁的页面布局

列表之美,简洁但不简单

原版的列表页纷乱嘈杂,到处充满着线线框框,有些楼层还时不时添上广告、嵌入块内容,这使原有的页面更加拥挤不堪,用户体验差。在新版的列表页中我 们对布局进行了整体的优化,使用色块铺底以区分各模块,从而精简原有的线框,使现有的页面整体性加强。线框的去除使得页面宽度能够更好的被利用,在保证原 有广告、嵌入块内容能够保持正常显示的情况下我们还加宽了左侧栏的宽度,力求展现给用户一个简洁清晰的结构。

更合理的引导模式

列表之美,简洁但不简单

WEB2.0时代的来临,衍生了许多优秀产物。Tag标签作为其中之一,在现行WEB页面上的运用已经日益成熟。为了引导用户快速定位自己所感兴趣 的信息,我们将贴子类型选择功能与原版的Tag标签功能进行整合,并在右侧配以搜索框加以辅助,功能上更具逻辑性,也更符合一般用户的操作习惯。

更贴心的功能设计

列表之美,简洁但不简单

我们还为用户汇集了19楼当下最热门的标签,十二个最常用的情感表达词汇,真正从用户角度出发,第一时间展示用户们所关心的内容,使更多的用户能够参与进来,感受19楼大家庭的温暖!还有许多其它的贴心功能设计就不一一介绍了,等待着大家去慢慢发现吧^_^

更舒适的阅读环境

列表之美,简洁但不简单

原版的列表内容字号太小,加上右侧图标元素过多,让用户在阅读的时候感觉很吃力。而这块内容恰恰又是列表页内用户使用率最高的一块,如何营造一个舒 适、友好的阅读环境成为问题的关键。最终我们将Google的“简洁”风格运用其上,抛弃原版的12px灰黑字体,统一改用14px蓝色字体,去除右侧多 余的图标元素,并对已有图标进行整合,使关键信息能以最佳形式表现在用户面前。真正做到“简洁但不简单”!

很荣幸能够参加这次19楼有史以来最彻底的改版,希望这次改版也能给19楼的新老用户带来更佳的用户体验,也希望19楼在以后会有越来越多的优秀产品推出,让我们坚信Yes,we can!

[MVC]一个Asp.net MVC 控件项目分析---Telerik.Web.Mvc

mikel阅读(641)

      在写本文之前,本人一直抱着‘不宜’在ASP.NET MVC框架下搞什么控件开发的想法,因为一提到控件就会让人想起‘事件’,‘VIEWSTATE’等一些问题,而ASP.NET MVC下是Controller, Action, Viewpage, Filter等特性的‘天下’。所以总感觉‘驴唇对不上马嘴’。

      但直到前阵子在邮箱中收到了关于telerik关于MVC框架扩展的一些信息之后,才发现这家商业控件公司也开始打MVC的主意了。而这个项目(开源)就 是该公司在理解了ASP.NET mvc的基础上所做的一些尝试,当然其所实现的所谓控件与之前我们在项目中所开发或使用的web服务器控件有很大的不同,可以说是抛弃了以往的设计方式。 尽管目前它的这种做法我心里还打着问号,但必定是一种尝试(不管你赞同还是不赞同)。下面就做一个简单的分析,希望能给研究MVC架构的朋友提供一些的思 考。

      首先要声明的是该开源项目中所使用的js就是JQuery,而那些显示效果也基本上就是基于JQuery中的那件插件为原型,并进行相应的属性封装,以便于在viewpage中用C#等语言进行声明绑定。下面就其中一些控件的显示截图:

      telerik_mvc_accordion

 

     telerik_mvc_date

 

     telerik_mvc_slider

      telerik_mvc_progressbar

 

       在该开源项目中,所有控件均基于JQueryViewComponentBase (abstract 类型),但其自身属性并不多,而所有的控件基类属性都被jQueryViewComponentBase 的父类ViewComponentBase所定义,下面以控件中的“Accordion(属性页控件)”为例进行说明,见下图:

 

telerik_mvc_baseclass

 

      上图中左侧的就是ViewComponentBase类,其定义了多数控件属性,比如js脚本名称和路径以及相关样式以及最终的html元素输出方法,因 为其类也是抽象类,所以其中大部分方法均为定义,而未进行具体实现。我们只要关注一下其构造方法就可以了:

 

  /// <summary>
    
/// View component base class.
    
/// </summary>
    public abstract class ViewComponentBase : IStyleableComponent, IScriptableComponent
    {
        
private string name;
        
private string styleSheetFilesLocation;
        
private string scriptFilesLocation;
        
/// <summary>
        
/// 初始化相关Initializes a new instance of the <see cref="ViewComponentBase"/> class.
        
/// </summary>
        
/// <param name="viewContext">当前视图的上下文,将会在子类中使用</param>
        
/// <param name="clientSideObjectWriterFactory">传入当前所使用的Writer工厂实例.通过子类注入,子类最终延伸到相对应的控件实例</param>
        protected ViewComponentBase(ViewContext viewContext, IClientSideObjectWriterFactory clientSideObjectWriterFactory)
        {
            Guard.IsNotNull(viewContext, 
"viewContext");
            Guard.IsNotNull(clientSideObjectWriterFactory, 
"clientSideObjectWriterFactory");
            ViewContext 
= viewContext;
            ClientSideObjectWriterFactory 
= clientSideObjectWriterFactory;
            StyleSheetFilesPath 
= WebAssetDefaultSettings.StyleSheetFilesPath;
            StyleSheetFileNames 
= new List<string>();
            ScriptFilesPath 
= WebAssetDefaultSettings.ScriptFilesPath;
            ScriptFileNames 
= new List<string>();
            HtmlAttributes 
= new RouteValueDictionary();
        }

 

    通过上述的构造方法,就可以将控件的一些通用默认属性值进行初始化了。

    下面以“Accordion”的源码来分析一下,这里还是从构造方法入手:

public class Accordion : jQueryViewComponentBase, IAccordionItemContainer
   {
       ……
       
/// <summary>
       
/// Initializes a new instance of the <see cref="Accordion"/> class.
       
/// </summary>
       
/// <param name="viewContext">The view context.</param>
       
/// <param name="clientSideObjectWriterFactory">The client side object writer factory.</param>
       public Accordion(ViewContext viewContext, IClientSideObjectWriterFactory clientSideObjectWriterFactory) : base(viewContext, clientSideObjectWriterFactory)
       {
           Items 
= new List<AccordionItem>();
           autoHeight 
= true;
       }

 

 

      注:上面的构程方法后面加入了base(viewContext, clientSideObjectWriterFactory),以实现向基类构造方法传参,也就是实现了上面所说的将当前控件所使用的 viewContext,clientSideObjectWriterFactory传递到基类ViewComponentBase 中去。(注:最终的clientSideObjectWriterFactory为ClientSideObjectWriterFactory实例类 型)。

      当然,因为该控件的中相应属性比较简单,只是一些set,get语法,所以就不过多介绍了,相信做过控件开发的对这些再熟悉不过了。

      下面主要介绍一下其write html元素时所使用的方法,如下:

    

 /// <summary>
      
/// 创建并写入初始化脚本对象和相应属性.
      
/// </summary>
      
/// <param name="writer">The writer.</param>
      public override void WriteInitializationScript(TextWriter writer)
      {
          
int selectedIndex = Items.IndexOf(GetSelectedItem());
          IClientSideObjectWriter objectWriter 
= ClientSideObjectWriterFactory.Create(Id, "accordion", writer);
          objectWriter.Start()
                      .Append(
"active", selectedIndex, 0)
                      .Append(
"animated", AnimationName)
                      .Append(
"autoHeight", AutoHeight, true)
                      .Append(
"clearStyle", ClearStyle, false)
                      .Append(
"collapsible", CollapsibleContent, false)
                      .Append(
"event", OpenOn)
                      .Append(
"fillSpace", FillSpace, false);
          
if (!string.IsNullOrEmpty(Icon) || !string.IsNullOrEmpty(SelectedIcon))
          {
              
if (!string.IsNullOrEmpty(Icon) && !string.IsNullOrEmpty(SelectedIcon))
              {
                  objectWriter.Append(
"icons:{'header':'" + Icon + "','headerSelected':'" + SelectedIcon + "'}");
              }
              
else if (!string.IsNullOrEmpty(Icon))
              {
                  objectWriter.Append(
"icons:{'header':'" + Icon + "'}");
              }
              
else if (!string.IsNullOrEmpty(SelectedIcon))
              {
                  objectWriter.Append(
"icons:{'headerSelected':'" + SelectedIcon + "'}");
              }
          }
          objectWriter.Append(
"change", OnChange).Complete();
          
base.WriteInitializationScript(writer);
      }

    

      可以看出,objectWriter (IClientSideObjectWriter 类型实例)中被绑定了相关的控件属性,并通过其类的WriteInitializationScript(writer)进行脚本的输出。而基本类的相应 方法如下:   

   /// <summary>
      
/// Writes the initialization script.
      
/// </summary>
      
/// <param name="writer">The writer.</param>
      public virtual void WriteInitializationScript(TextWriter writer)
      {
      }

 

  

      大家看到该方法为空,但其又是如何运行起来的呢,这里先卖个关子,稍后再说。接着再看一下另一个方法:WriteHtml()
   

  /// <summary>
      
/// 输出当前的 HTML代码.
      
/// </summary>
      protected override void WriteHtml()
      {
          AccordionItem selectedItem 
= GetSelectedItem();
          TextWriter writer 
= ViewContext.HttpContext.Response.Output;
          
if (!string.IsNullOrEmpty(Theme))
          {
              writer.Write(
"<div class=\"{0}\">".FormatWith(Theme));
          }
          HtmlAttributes.Merge(
"id", Id, false);
          HtmlAttributes.AppendInValue(
"class"" ""ui-accordion ui-widget ui-helper-reset");
          writer.Write(
"<div{0}>".FormatWith(HtmlAttributes.ToAttributeString()));
          
foreach (AccordionItem item in Items)
          {
              item.HtmlAttributes.AppendInValue(
"class"" ""ui-accordion-header ui-helper-reset ui-state-default ");
              item.ContentHtmlAttributes.AppendInValue(
"class"" ""ui-accordion-content ui-helper-reset ui-widget-content ui-corner-bottom");
              
if (item == selectedItem)
              {
                  item.ContentHtmlAttributes.AppendInValue(
"class"" ""ui-accordion-content-active");
              }
              
else
              {
                  item.HtmlAttributes.AppendInValue(
"class"" ""ui-corner-all");
              }
              writer.Write(
"<h3{0}><a href=\"#\">{1}</a></h3>".FormatWith(item.HtmlAttributes.ToAttributeString(), item.Text));
              item.ContentHtmlAttributes.AppendInValue(
"style"";", (item == selectedItem) ? "display:block" : "display:none");
              writer.Write(
"<div{0}>".FormatWith(item.ContentHtmlAttributes.ToAttributeString()));
              item.Content();
              writer.Write(
"</div>");
          }
          writer.Write(
"</div>");
          
if (!string.IsNullOrEmpty(Theme))
          {
              writer.Write(
"</div>");
          }
          
base.WriteHtml();
      }

     

      该方法首先获取当前所选属性页标签(GetSelectedItem()方法),然后用foreach方法对属性页标签集合进行遍历,并判断当前属性页是 否就是被选中的属性页,并绑定上相应的css属性。其最终也是调用相应的基类方法进行输出。当然这里基类方法也是为空,呵呵。

 

     准备好了这个控件类之后,Telerik还为Accordion控件‘准备’了一些辅助组件,比如属性页组件(AccordionItem),以及相关的 组件构造器(AccordionItemBuilder,AccordionBuilder),这样我们就可以通过这些构造器很方便的创建相应的控件和组 件了,下面就以AccordionItemBuilder为例,解释一下其构造器结构:

Code

 

     对于上面的OnChange方法,可以使用下面的方法将相应的js脚本传入并执行

.OnChange(() =>
                                    {
%>
                                        function(
event, ui)
                                        {
                                            $(
'#trace').append('Change fired: ' + new Date() + '<br/>');
                                        }
                                    
<%}
                              )

 

    这样,当属性页发生变化时,就会在页面的指定区域将变化时间显示出来了,如下图:

   telerik_mvc_onchange

  

    Telerik在jQueryViewComponentFactory中对项目中每一个控件提供了一个方法用以初始化相应的构造器,以便于创建相应的控件,比如Accordion,形如: 

   /// <summary>
     
/// Creates a accordion for ASP.NET MVC view.
     
/// </summary>
     
/// <returns></returns>
     [DebuggerStepThrough]
     
public virtual AccordionBuilder Accordion()
     {
         
return new AccordionBuilder(Create(() => new Accordion(ViewContext, clientSideObjectWriterFactory)));
     }

 

 

    而对于其在VIEW中的使用,则通过扩展方法来加以声明:
  

public static class HtmlHelperExtension
   {
       
private static readonly IClientSideObjectWriterFactory factory = new ClientSideObjectWriterFactory();
       
/// <summary>
       
/// Gets the jQuery view components instance.
       
/// </summary>
       
/// <param name="helper">The html helper.</param>
       
/// <returns>jQueryViewComponentFactory</returns>
       [DebuggerStepThrough]
       
public static jQueryViewComponentFactory jQuery(this HtmlHelper helper)
       {
           
return new jQueryViewComponentFactory(helper, factory);
       }
   }

 

    这样在页面视图中,我们这可以使用下面的写法来构造一个Accordion控件了:

<% Html.jQuery().Accordion()
                  .Name(
"myAccordion")
                  .Animate(
"bounceslide")
                  .Items(parent 
=>
  ……

 

     上面只是介绍了前台和底层代码如果显示的问题,但还没有解释之前所说的WriteInitializationScript(TextWriter writer)方法以及WriteHtml()
方法如何被调用的问题,正如之前所看到的,因为Accordion的基类ViewComponentBase中未实现具体的代码,所以这里我们要将注意力转移到 jQueryViewComponentFactory中,请看如下代码: 

private TViewComponent Create<TViewComponent>(Func<TViewComponent> factory) where TViewComponent : ViewComponentBase
       {
           TViewComponent component 
= factory();
           
if (component is jQueryViewComponentBase)
           {
               component.AssetKey 
= DefaultAssetKey;
           }
           htmlHelper.Telerik().StyleSheetRegistrar().ToRegistrar().Register(component);
           htmlHelper.Telerik().ScriptRegistrar().ToRegistrar().Register(component);
           
return component;
       }

 

     上面的方法其实就是之前在该类方法Accordion()中所调用并执行的:

     return new AccordionBuilder(Create(() => new Accordion(ViewContext, clientSideObjectWriterFactory)));

     通过该方法,就可以将该控件及其相关组件信息注册到相应的视图中。因为我们比较关注WriteHtml()方法,所以这里就直接分析一下这一行代码:    

     ScriptRegistrar().ToRegistrar().Register(component);

 

     ScriptRegistrar类中的Register方法承担着将当前要创建的组件添加到当前的脚本组件列表中的任务(scriptableComponents为list列表)    

       /// <summary>
       
/// Registers the scriptable component.
       
/// </summary>
       
/// <param name="component">The component.</param>
       public virtual void Register(IScriptableComponent component)
       {
           Guard.IsNotNull(component, 
"component");
           
if (!scriptableComponents.Contains(component))
           {
               scriptableComponents.Add(component);
           }
       }

    

      当组件被成功添加到该list列表中后,系统就会调用Render()方法将其显示出来(注:该方法与以前web控件开发中的显示方法同名,所以比较好理解),如下:   

      /// <summary>
      
/// Writes the scripts in the response.
      
/// </summary>
      public void Render()
      {
          
if (hasRendered)
          {
              
throw new InvalidOperationException(Resources.TextResource.YouCannotCallRenderMoreThanOnce);
          }
          
if (ViewContext.HttpContext.Request.Browser.EcmaScriptVersion.Major >= 1)
          {
              Write(ViewContext.HttpContext.Response.Output);
          }
          hasRendered 
= true;
      }

 

     注意上面的这一行代码:

   Write(ViewContext.HttpContext.Response.Output);

 

      其所实现的功能如下:  

      /// <summary>
      
/// 写出所有脚本资源和脚本 statements.
      
/// </summary>
      
/// <param name="writer">The writer.</param>
      protected virtual void Write(TextWriter writer)
      {
          WriteScriptSources(writer);
          WriteScriptStatements(writer);
      }

       而就是WriteScriptStatements这行代码开始执行之前所说的那个 WriteInitializationScript(TextWriter writer)。而WriteHtml()方法的执行入口要更加复杂一些,因为Telerik提供了ViewComponentBuilderBase这 个类来进行视图组件的构造,而该类中的Render方法就是对相应组件的Render方法(组件中已定义)进行调用,如下:

    /// <summary>
    
/// Renders the component.
    
/// </summary>
    public virtual void Render()
    {
        Component.Render();
    }

 

      而之前的“Accordion”控件是继承自ViewComponentBase类,所以相应组件的Render方法就在该类中进行了声明定义,如下:

     /// <summary>
     
/// Renders the component.
     
/// </summary>
     public void Render()
     {
         EnsureRequired();
         WriteHtml();
     }

 

      大家看到了第二行代码了吧,这就是我们之前看到的那个方法,也就是Accordion组件中WriteHtml()重写方法的调用入口。

 

      绕了这么一大圈,才把这个流程理清,是不是有些晕了。的确,刚开始接触时我也有点晕,但晕呀晕呀就‘晕过去了’,现在再回头看起来感常见其整体的架构思路还是很清晰的。可以说有了这瓶酒垫底,再分析该项目中的其它控件就‘如鱼得水’了。

 

      最后不妨总结一下:

      该项目中对asp.net mvc控件下的开发做了一次尝试,但如果之前做过控件特别是web服务器端控件开发的朋友,可以看出项目中有非常重的web控件开发味道,基本连方法名称都有一定的重叠。

      另外就是其自身还是引用了组件对象模型的概念,就拿属性页控件来说,就将其分为Accordion和AccordionItem两种类型,其中可以将 Accordion看成是AccordionItem的集合封装(包括遍历操作),而这里AccordionItem就成了Accordion的一个组 件,而Accordion又是当前view中的一个组件。而组件开发一直是.net平台上所倡导的。其优势在于可复用,维护方便,简化复杂问题等。

 

      好了,今天的内容就先到这里了。

      原文链接:http://www.cnblogs.com/daizhj/archive/2009/09/09/1562966.html

      作者: daizhj, 代震军

      Tags: asp.net,mvc,component,组件,控件,Telerik

      网址: http://daizhj.cnblogs.com/