Yii中用Gii生成的admin视图是用CGridView这个Zii组件实现的,它的添加、更新操作仍然是转换到create、update视图来完成的,而删除操作是Ajax方式,从而可以实现连续删除(因为admin视图“不动”)。
当然,实际控制器代码中为不支持Ajax的情况(虽然现在禁用javascript的情况较少出现了)留了后路,即非ajax方式,删除后跳转到预设的返回URL或admin:
if (!isset($_POST['ajax'])) $this->redirect(isset($_POST['returnUrl']) ? $_POST['returnUrl'] : array('admin'));
还是来看看admin视图中“调用”CGridView的情况:
$this->widget('zii.widgets.grid.CGridView', array( 'id' => 'post-grid', 'dataProvider' => $model->search(), 'filter' => $model, 'columns' => array( …………………… array('class' => 'CButtonColumn', ), ), ));
单元格组件的最后一列(按钮列)是通过类CButtonColumn来实现的——Yii中定义数据值的方式常常有3种,即直接定义值、方法定义值、类定义值
CButtonColumn类的使用方法,http://blog.sina.com.cn/s/blog_795914e701014sav.html 有较详细的论述,这里对其进行进一步理解
默认CButtonColumn类呈现3个按钮,从直观上理解,每个按钮都有对应的图片、鼠标移上去显示的提示文字、状态栏可见的对应URL,所以,每个按钮包含了XxxButtonImageUrl 、XxxButtonLabel、XxxButtonUrl 属性,还有XxxButtonOptions属性 (按钮的HTML选项,像其他插件的htmlOptions 属性一样)。删除按钮略特殊,可以使用deleteConfirmation属性(删除前的提示信息)和类事件属性afterDelete。XxxButtonUrl 属性中可以使用$row、$data、$this等变量来定制目标url。
CButtonColumn的灵活性在于可以通过模板实现按钮定制,定制的基本思路是:
array ( 'class'=>'CButtonColumn', 'template'=>'{id1}{id2}{delete}', 'buttons'=>array ( 'id1' => array ( 'label'=>'功能1', 'imageUrl'=>Yii::app()->request->baseUrl.'/images/id1.png', 'url'=>'Yii::app()->createUrl("users/id1", array("id"=>$data->id))', ), 'id2' => array ( 'label'=>'功能2', 'url'=>'"#"', 'visible'=>'$data->score > 0', 'click'=>'function(){alert("Going down!");}', ), ), ),
即:指明类CButtonColumn,然后用template参数指明所有按钮id(update、view、delete等我默认类可识别的id),用buttons数组指明每个按钮对应的有关属性(默认的按钮可以不进行属性设定,使用它本身默认的功能,如上面的delete)。
一些细节用法:
*指定按钮列头部的显示文字:用'header' => '操作', 进行属性设定即可
*隐藏某个按钮:类似'updateButtonOptions'=>array('style'=>'display:none'),
*每个按钮可设定的有关属性
'buttonID' => array
(
'label'=>'...', //按钮的文本标签.
'url'=>'...', //使用 PHP 表达式得出按钮的 URL.
'imageUrl'=>'...', //按钮的图片路径.
'options'=>array(), //按钮的 HTML 选项.
'click'=>'...', //当点击按钮时调用的 javascript 函数
'visible'=>'...', //确定按钮是否显示的 PHP 表达式
)
*删除的确定信息中包含和该行实例相关的信息:参考以下用法
array
(
'class'=>'CButtonColumn',
'deleteConfirmation'=>"js:'Record with ID '+$(this).parent().parent().children(':first-child').text()+' will be deleted! Continue?'",
),
或
array
(
'class'=>'CButtonColumn',
'deleteConfirmation'=>"js:'Do you really want to delete record with ID '+$(this).parent().parent().children(':nth-child(2)').text()+'?'",
),
由于Gii生成的是默认按钮,仅仅指定了类名,要理解默认的删除的动作,还是需要了解该类源码。
用一定的IDE,可以从源码清晰看到,CButtonColumn类从CGridColumn继承,有很多(19个)自己的变量,但只有少量(5个)自己的函数:init()、initDefaultButtons()、registerClientScript()、renderButton、renderDataCellContent
init()用于初始化列(为按钮列注册必要的客户端Javascript脚本):先调用initDefaultButtons初始化默认按钮,再设定每个按钮对应的click事件处理代码,最后用registerClientScript注册Javascript脚本。
initDefaultButtons()用于初始化默认的三个按钮:设定默认按钮的Label、ImageUrl等,构建buttons数组。其中对delete按钮的设定明显较复杂,其中对delete的click事件处理代码进行了设置,如下:
if(!isset($this->buttons['delete']['click']))
{
if(is_string($this->deleteConfirmation))
$confirmation="if(!confirm(".CJavaScript::encode($this->deleteConfirmation).")) return false;";
…………………………………………
if($this->afterDelete===null)
$this->afterDelete='function(){}';
$this->buttons['delete']['click']=<<<EOD function() { $confirmation var th = this, afterDelete = $this->afterDelete; jQuery('#{$this->grid->id}').yiiGridView('update', { type: 'POST', url: jQuery(this).attr('href'),$csrf success: function(data) { jQuery('#{$this->grid->id}').yiiGridView('update'); afterDelete(th, true, data); }, error: function(XHR) { return afterDelete(th, false, XHR); } }); return false; } EOD;
afterDelete变量获取用户设定的处理过程(如果用户未设定,就是个空函数)。用jQuery选择符选择该单元格并调用其yiiGridView函数——从结构来看yiiGridView应该是一个Ajax的POST方式的update过程,最后返回false结束事件处理。显然,这部分是要在删除按钮点击后发送删除请求,在成功后刷新单元格显示。这里的关键是yiiGridView函数,并且它使用了两次。
registerClientScript就是注册客户端脚本,这个查阅权威指南可以了解客户端脚本的分布和注册的。
renderButton是根据参数如何渲染按钮,它在renderDataCellContent中被调用,最终渲染出整列。
我们从哪里来了解用于单元格的Javascript函数yiiGridView呢?
从框架的目录zii/widgets/assets/gridview下可以发现用于单元格组件的js脚本jquery.yiigridview.js文件,它是作为jQuery的插件形式设计的。
因为该插件的代码是针对整个单元格组件的功能来说的,我们只挑选对删除按钮相关的部分来了解:
$.fn.yiiGridView = function (method) { if (methods[method]) { return methods[method].apply(this, Array.prototype.slice.call(arguments, 1)); } else if (typeof method === 'object' || !method) { return methods.init.apply(this, arguments); } else { $.error('Method ' + method + ' does not exist on jQuery.yiiGridView'); return false; } };yiiGridView函数是一个方法“调用器”,它的第一个参数method是被调用的方法(如果在方法列表中)或对象(不在方法列表中)。methods属性很长,我们挑键为update的部分来看:
update: function (options) { var customError; if (options && options.error !== undefined) { customError = options.error; delete options.error; } return this.each(function () { var $form, $grid = $(this), id = $grid.attr('id'), settings = gridSettings[id]; options = $.extend({ type: settings.ajaxType, url: $grid.yiiGridView('getUrl'), success: function (data) { var $data = $('<div>' + data + '</div>'); $.each(settings.ajaxUpdate, function (i, el) { var updateId = '#' + el; $(updateId).replaceWith($(updateId, $data)); }); if (settings.afterAjaxUpdate !== undefined) { settings.afterAjaxUpdate(id, data); } if (settings.selectableRows > 0) { selectCheckedRows(id); } }, complete: function () { yiiXHR[id] = null; $grid.removeClass(settings.loadingClass); }, error: function (XHR, textStatus, errorThrown) { var ret, err; if (XHR.readyState === 0 || XHR.status === 0) { return; } if (customError !== undefined) { ret = customError(XHR); if (ret !== undefined && !ret) { return; } } switch (textStatus) { case 'timeout': err = 'The request timed out!'; break; case 'parsererror': err = 'Parser error!'; break; case 'error': if (XHR.status && !/^\s*$/.test(XHR.status)) { err = 'Error ' + XHR.status; } else { err = 'Error'; } if (XHR.responseText && !/^\s*$/.test(XHR.responseText)) { err = err + ': ' + XHR.responseText; } break; } if (settings.ajaxUpdateError !== undefined) { settings.ajaxUpdateError(XHR, textStatus, errorThrown, err); } else if (err) { alert(err); } } }, options || {}); if (options.type === 'GET') { if (options.data !== undefined) { options.url = $.param.querystring(options.url, options.data); options.data = {}; } } else { if (options.data === undefined) { options.data = $(settings.filterSelector).serialize(); } } if(yiiXHR[id] != null){ yiiXHR[id].abort(); } //class must be added after yiiXHR.abort otherwise ajax.error will remove it $grid.addClass(settings.loadingClass); if (settings.ajaxUpdate !== false) { if(settings.ajaxVar) { options.url = $.param.querystring(options.url, settings.ajaxVar + '=' + id); } if (settings.beforeAjaxUpdate !== undefined) { settings.beforeAjaxUpdate(id, options); } yiiXHR[id] = $.ajax(options); } else { // non-ajax mode if (options.type === 'GET') { window.location.href = options.url; } else { // POST mode $form = $('<form action="' + options.url + '" method="post"></form>').appendTo('body'); if (options.data === undefined) { options.data = {}; } if (options.data.returnUrl === undefined) { options.data.returnUrl = window.location.href; } $.each(options.data, function (name, value) { $form.append($('<input type="hidden" name="t" value="" />').attr('name', name).val(value)); }); $form.submit(); } } }); },我们知道索引为update的值是一个function,它的参数options对应jQuery的AJAX请求中的选项,所以我们前面的用法中看到和jQuery Ajax请求相似的格式。
update的作用是执行基于Ajax的单元格内容的更新,默认情况,请求的URL是产生当前单元格视图对应的那个URL,返回值为jQuery对象。
对于使用者来说,再细究细节没有太大意义。我们来了解$.ajax的有关选项。
type参数指定请求方式,默认是GET,上面的删除中使用POST方式。
url参数是发送请求的目的地址,默认当前页地址。注意,前面的代码中,url第一次是:
url: jQuery(this).attr('href'),$csrf 它是指向类似href="/blog/index.php?r=post/delete&id=1"的URL的,所以,其实是执行actionDelete($id)。url第二次是:
jQuery('#{$this->grid->id}').yiiGridView('update'); 它没有指定url,会使用url: $grid.yiiGridView('getUrl'), 即由方法getUrl确定:
getUrl: function () {
var sUrl = gridSettings[this.attr('id')].url;
return sUrl || this.children('.keys').attr('title');
},
应该就是admin视图
如果要处理 $.ajax() 得到的数据,则需要使用回调函数:beforeSend、error、dataFilter、success、complete。上面用到了success和error 。success的时候进行了上面第二个url的调用,即刷新过程。
admin视图中另一个Ajax操作是高级搜索表单:
<?php echo CHtml::link('高级搜索','#',array('class'=>'search-button')); ?>
<div class="search-form" style="display:none">
<?php $this->renderPartial('_search',array(
'model'=>$model,
)); ?>
</div><!-- search-form -->
因为表单初始是不显示的,所以提供了链接按钮“高级搜索”来显示/隐藏切换。对应注册的客户端Javascript脚本包含了显示/隐藏切换代码和Ajax方式提交表单的代码:
Yii::app()->clientScript->registerScript('search', "
$('.search-button').click(function(){
$('.search-form').toggle();
return false;
});
$('.search-form form').submit(function(){
$('#user-grid').yiiGridView('update', {
data: $(this).serialize()
});
return false;
});
");
在表单提交中,我们再次见到了方法调用器yiiGridView和被调用的函数update,指定了选项data为对表单数据的序列化打包。