整合营销服务商

电脑端+手机端+微信端=数据同步管理

免费咨询热线:

CKEditor系列(三)粘贴操作是怎么完成的

上一篇文章CKEditor系列(二)事件系统是怎么实现的中,我们了解了CKEditor中事件系统的运行流程,我们先简单回顾下:

  • 用户注册回调函数时可以指定优先级,值越小的优先级越高,默认是10
  • 系统会根据用户的传参组装成系统规范的回调函数,供后续执行
  • 执行回调函数时可以将取消事件和阻止事件,不让其它监听该事件的回调函数执行。

当插件希望对paste事件进行响应,一般有两种方式可供选择。

直接监听'paste'事件

默认情况下,插件clipboard插件是监听paste事件最多的。 我们可以看到里面多次出现类似这样的代码

// plugins/clipboard/plugin.js
editor.on( 'paste', function( evt ) {

})

我们可以看到里面有几个优先级priority 为1回调

处理粘贴图片的场景

将png、jpg、gif图片的内容base64信息赋值给evt.data.dataValue

editor.on( 'paste', function( evt ) {
    var dataObj = evt.data,
        data = dataObj.dataValue,
        dataTransfer = dataObj.dataTransfer;

    // If data empty check for image content inside data transfer. https://dev.ckeditor.com/ticket/16705
    // Allow both dragging and dropping and pasting images as base64 (#4681).
    if ( !data && isFileData( evt, dataTransfer ) ) {
        var file = dataTransfer.getFile( 0 );
        if ( CKEDITOR.tools.indexOf( supportedImageTypes, file.type ) != -1 ) {
            var fileReader = new FileReader();

            // Convert image file to img tag with base64 image.
            fileReader.addEventListener( 'load', function() {
                evt.data.dataValue = '<img src="' + fileReader.result + '" />';
                editor.fire( 'paste', evt.data );
            }, false );

            // Proceed with normal flow if reading file was aborted.
            fileReader.addEventListener( 'abort', function() {
                // (#4681)
                setCustomIEEventAttribute( evt );
                editor.fire( 'paste', evt.data );
            }, false );

            // Proceed with normal flow if reading file failed.
            fileReader.addEventListener( 'error', function() {
                // (#4681)
                setCustomIEEventAttribute( evt );
                editor.fire( 'paste', evt.data );
            }, false );

            fileReader.readAsDataURL( file );

            latestId = dataObj.dataTransfer.id;

            evt.stop();
        }
    }
}, null, null, 1 );

因为base64信息需要通过fileReader来处理:在图片的load回调里面才能拿到,所以我们需要先执行evt.stop(),避免其它回调被执行了,然后在图片load的回调里面重新触发一直paste事件 editor.fire( 'paste', evt.data );,对应的aborterror也要触发,避免因图片失败,导致其它回调都没机会执行了。

该回调会在下一轮paste回调执行中再次执行吗?不会,因为该回调首次执行时evt.data.dataValue为空,下次执行时evt.data.dataValue已经被上次执行给赋值了,不会重复执行fileReader相关处理了。

数据准备

editor.on( 'paste', function( evt ) {
    // Init `dataTransfer` if `paste` event was fired without it, so it will be always available.
    if ( !evt.data.dataTransfer ) {
        evt.data.dataTransfer = new CKEDITOR.plugins.clipboard.dataTransfer();
    }

    // If dataValue is already set (manually or by paste bin), so do not override it.
    if ( evt.data.dataValue ) {
        return;
    }

    var dataTransfer = evt.data.dataTransfer,
        // IE support only text data and throws exception if we try to get html data.
        // This html data object may also be empty if we drag content of the textarea.
        value = dataTransfer.getData( 'text/html' );

    if ( value ) {
        evt.data.dataValue = value;
        evt.data.type = 'html';
    } else {
        // Try to get text data otherwise.
        value = dataTransfer.getData( 'text/plain' );

        if ( value ) {
            evt.data.dataValue = editor.editable().transformPlainTextToHtml( value );
            evt.data.type = 'text';
        }
    }
}, null, null, 1 );

可以看到这个回调函数是主要是给evt.data增加dataTransferdataValue(如果已经被其它插件设置了就直接return出去)和type的,是做准备工作的,所以这个回调函数自然需要最先执行,优先级设置为1。

看看第二个回调函数

解决兼容性

editor.on( 'paste', function( evt ) {
    var data = evt.data.dataValue,
        blockElements = CKEDITOR.dtd.$block;

    // Filter webkit garbage.
    if ( data.indexOf( 'Apple-' ) > -1 ) {
        // Replace special webkit's   with simple space, because webkit
        // produces them even for normal spaces.
        data = data.replace( /<span class="Apple-converted-space"> <\/span>/gi, ' ' );

        // Strip <span> around white-spaces when not in forced 'html' content type.
        // This spans are created only when pasting plain text into Webkit,
        // but for safety reasons remove them always.
        if ( evt.data.type != 'html' ) {
            data = data.replace( /<span class="Apple-tab-span"[^>]*>([^<]*)<\/span>/gi, function( all, spaces ) {
                // Replace tabs with 4 spaces like Fx does.
                return spaces.replace( /\t/g, '    ' );
            } );
        }

        // This br is produced only when copying & pasting HTML content.
        if ( data.indexOf( '<br class="Apple-interchange-newline">' ) > -1 ) {
            evt.data.startsWithEOL = 1;
            evt.data.preSniffing = 'html'; // Mark as not text.
            data = data.replace( /<br class="Apple-interchange-newline">/, '' );
        }

        // Remove all other classes.
        data = data.replace( /(<[^>]+) class="Apple-[^"]*"/gi, '$1' );
    }

    // Strip editable that was copied from inside. (https://dev.ckeditor.com/ticket/9534)
    if ( data.match( /^<[^<]+cke_(editable|contents)/i ) ) {
        var tmp,
            editable_wrapper,
            wrapper = new CKEDITOR.dom.element( 'div' );

        wrapper.setHtml( data );
        // Verify for sure and check for nested editor UI parts. (https://dev.ckeditor.com/ticket/9675)
        while ( wrapper.getChildCount() == 1 &&
                ( tmp = wrapper.getFirst() ) &&
                tmp.type == CKEDITOR.NODE_ELEMENT &&    // Make sure first-child is element.
                ( tmp.hasClass( 'cke_editable' ) || tmp.hasClass( 'cke_contents' ) ) ) {
            wrapper = editable_wrapper = tmp;
        }

        // If editable wrapper was found strip it and bogus <br> (added on FF).
        if ( editable_wrapper )
            data = editable_wrapper.getHtml().replace( /<br>$/i, '' );
    }

    if ( CKEDITOR.env.ie ) {
        //   <p> -> <p> (br.cke-pasted-remove will be removed later)
        data = data.replace( /^ (?: |\r\n)?<(\w+)/g, function( match, elementName ) {
            if ( elementName.toLowerCase() in blockElements ) {
                evt.data.preSniffing = 'html'; // Mark as not a text.
                return '<' + elementName;
            }
            return match;
        } );
    } else if ( CKEDITOR.env.webkit ) {
        // </p><div><br></div> -> </p><br>
        // We don't mark br, because this situation can happen for htmlified text too.
        data = data.replace( /<\/(\w+)><div><br><\/div>$/, function( match, elementName ) {
            if ( elementName in blockElements ) {
                evt.data.endsWithEOL = 1;
                return '</' + elementName + '>';
            }
            return match;
        } );
    } else if ( CKEDITOR.env.gecko ) {
        // Firefox adds bogus <br> when user pasted text followed by space(s).
        data = data.replace( /(\s)<br>$/, '$1' );
    }

    evt.data.dataValue = data;
}, null, null, 3 );

从上面的代码很容易看出,主要是针对不同的浏览器做一下兼容性相关的处理,具体细节我们不用太关心

针对不同粘贴源进行数据过滤

editor.on( 'paste', function( evt ) {
    var dataObj = evt.data,
        type = editor._.nextPasteType || dataObj.type,
        data = dataObj.dataValue,
        trueType,
        // Default is 'html'.
        defaultType = editor.config.clipboard_defaultContentType || 'html',
        transferType = dataObj.dataTransfer.getTransferType( editor ),
        isExternalPaste = transferType == CKEDITOR.DATA_TRANSFER_EXTERNAL,
        isActiveForcePAPT = editor.config.forcePasteAsPlainText === true;

    // If forced type is 'html' we don't need to know true data type.
    if ( type == 'html' || dataObj.preSniffing == 'html' ) {
        trueType = 'html';
    } else {
        trueType = recogniseContentType( data );
    }

    delete editor._.nextPasteType;

    // Unify text markup.
    if ( trueType == 'htmlifiedtext' ) {
        data = htmlifiedTextHtmlification( editor.config, data );
    }

    // Strip presentational markup & unify text markup.
    // Forced plain text (dialog or forcePAPT).
    // Note: we do not check dontFilter option in this case, because forcePAPT was implemented
    // before pasteFilter and pasteFilter is automatically used on Webkit&Blink since 4.5, so
    // forcePAPT should have priority as it had before 4.5.
    if ( type == 'text' && trueType == 'html' ) {
        data = filterContent( editor, data, filtersFactory.get( 'plain-text' ) );
    }
    // External paste and pasteFilter exists and filtering isn't disabled.
    // Or force filtering even for internal and cross-editor paste, when forcePAPT is active (#620).
    else if ( isExternalPaste && editor.pasteFilter && !dataObj.dontFilter || isActiveForcePAPT ) {
        data = filterContent( editor, data, editor.pasteFilter );
    }

    if ( dataObj.startsWithEOL ) {
        data = '<br data-cke-eol="1">' + data;
    }
    if ( dataObj.endsWithEOL ) {
        data += '<br data-cke-eol="1">';
    }

    if ( type == 'auto' ) {
        type = ( trueType == 'html' || defaultType == 'html' ) ? 'html' : 'text';
    }

    dataObj.type = type;
    dataObj.dataValue = data;
    delete dataObj.preSniffing;
    delete dataObj.startsWithEOL;
    delete dataObj.endsWithEOL;
}, null, null, 6 );

这个主要是根据不同的typetrueType来对数据进行一些过滤操作

插入粘贴数据

粘贴的数据总得进入到编辑器吧,这就靠它了。

editor.on( 'paste', function( evt ) {
    var data = evt.data;
    if ( data.dataValue ) {
        editor.insertHtml( data.dataValue, data.type, data.range );

        // Defer 'afterPaste' so all other listeners for 'paste' will be fired first.
        // Fire afterPaste only if paste inserted some HTML.
        setTimeout( function() {
            editor.fire( 'afterPaste' );
        }, 0 );
    }
}, null, null, 1000 );

这个就比较简单了,但是也很重要,等paste事件系统的回调函数和用户添加的回调函数执行完毕后,这个回调函数作为最后执行的(如果前面的回调函数没有执行evt.stop()或者evt.cancel()),将evt.data.dataValue的值插入到编辑器中。

我们可以再多看一下/plugins/clipboard/plugin.js文件,里面有个对工具栏增加粘贴按钮,加上pasteCommand的操作

{

    exec: function( editor, data ) {
    data = typeof data !== 'undefined' && data !== null ? data : {};

    var cmd = this,
        notification = typeof data.notification !== 'undefined' ? data.notification : true,
        forcedType = data.type,
        keystroke = CKEDITOR.tools.keystrokeToString( editor.lang.common.keyboard,
            editor.getCommandKeystroke( this ) ),
        msg = typeof notification === 'string' ? notification : editor.lang.clipboard.pasteNotification
            .replace( /%1/, '<kbd aria-label="' + keystroke.aria + '">' + keystroke.display + '</kbd>' ),
        pastedContent = typeof data === 'string' ? data : data.dataValue;

    function callback( data, withBeforePaste ) {
        withBeforePaste = typeof withBeforePaste !== 'undefined' ? withBeforePaste : true;

        if ( data ) {
            data.method = 'paste';

            if ( !data.dataTransfer ) {
                data.dataTransfer = clipboard.initPasteDataTransfer();
            }

            firePasteEvents( editor, data, withBeforePaste );
        } else if ( notification && !editor._.forcePasteDialog ) {
            editor.showNotification( msg, 'info', editor.config.clipboard_notificationDuration );
        }

        // Reset dialog mode (#595).
        editor._.forcePasteDialog = false;

        editor.fire( 'afterCommandExec', {
            name: 'paste',
            command: cmd,
            returnValue: !!data
        } );
    }

    // Force type for the next paste. Do not force if `config.forcePasteAsPlainText` set to true or 'allow-word' (#1013).
    if ( forcedType && editor.config.forcePasteAsPlainText !== true && editor.config.forcePasteAsPlainText !== 'allow-word' ) {
        editor._.nextPasteType = forcedType;
    } else {
        delete editor._.nextPasteType;
    }

    if ( typeof pastedContent === 'string' ) {
        callback( {
            dataValue: pastedContent
        } );
    } else {
        editor.getClipboardData( callback );
    }
}

上面的callback会执行firePasteEvents,然后触发paste事件。 如果pastedContent不是字符串的话,会先执行 editor.getClipboardData,该方法中有一个目前看到的优先级最好的paste回调

editor.on( 'paste', onPaste, null, null, 0 );

function onPaste( evt ) {
    evt.removeListener();
    evt.cancel();
    callback( evt.data );
}

onPaste方法里面会移除当前的回调函数,并取消掉后面未执行的paste回调,然后执行callback,也就是说它会触发一轮新的paste回调函数执行。

通过pasteTools插件来注册paste回调

{
    register: function(definition) {
        if (typeof definition.priority !== 'number')
        {
            definition.priority = 10;
        }

        this.handlers.push(definition);
    },
    addPasteListener: function( editor ) {
        editor.on( 'paste', function( evt ) {
            var handlers = getMatchingHandlers( this.handlers, evt ),
                filters,
                isLoaded;

            if ( handlers.length === 0 ) {
                return;
            }

            filters = getFilters( handlers );

            isLoaded = loadFilters( filters, function() {
                return editor.fire( 'paste', evt.data );
            } );

            if ( !isLoaded ) {
                return evt.cancel();
            }

            handlePaste( handlers, evt );
        }, this, null, 3 );
    }
}
...
function getMatchingHandlers( handlers, evt ) {
    return CKEDITOR.tools.array.filter( handlers, function( handler ) {
        return handler.canHandle( evt );
    } ).sort( function( handler1, handler2 ) {
        if ( handler1.priority === handler2.priority ) {
            return 0;
        }

        return handler1.priority - handler2.priority;
    } );
}

function handlePaste( handlers, evt ) {
    var handler = handlers.shift();

    if ( !handler ) {
        return;
    }

    handler.handle( evt, function() {
        handlePaste( handlers, evt );
    } );
}

这个会把通过它注册的回调函数放进自己的handlers里面,而不跟上面那些直接监听paste放在一起,只有该组件自身才监听paste事件,优先级为3。这等于是将通过pasteTools.register注册的这一组回调全部按照了优先级为3的顺序来执行了,当然,这一组的回调直接同样按照优先级高低来执行,并且会根据其canHandle方法返回的值来过滤该回调是否执行,通过其handle来执行回调逻辑。

通过对源码的搜索,发现CKEditor大部分官方提供的对粘贴进行干预的插件都是通过pasteTools.register注册的。

总结

通过对pasteTools插件的学习,我们可以对自己想做系统级事件和用户级事件的分离的方式多一点启发,我们假设editor.on('paste')这种模式是系统级别的,只允许系统级插件有这种操作,而用户级插件不行,用户级插件只能通过系统级插件pasteTools暴露出来的register来注册,我们可以根据用户级插件的canHandle方法来让该插件只处理自己希望处理的那一部分。 类似的这种分离方法,也能更好地降低用户级插件对整个系统的影响

文内容较多,建议收藏阅读。

ASP.NET MVC采用Model绑定为目标Action生成了相应的参数列表,但是在真正执行目标Action方法之前,还需要对绑定的参数实施验证以确保其有效性,我们将针对参数的验证成为Model绑定。总地来说,我们可以采用4种不同的编程模式来进行针对绑定参数的验证。

一、手工验证绑定的参数

我们通过一个简单的实例来演示如何将参数验证逻辑实现在对应的Action方法中,并在没有通过验证的情况下将错误信息响应给客户端。我们在一个ASP.NET MVC应用中定义了如下一个Person类作为被验证的数据类型,它的Name、Gender和Age三个属性分别表示一个人的姓名、性别和年龄。

 1: public class Person
 2: {
 3: [DisplayName("姓名")]
 4: public string Name { get; set; }
 5: 
 6: [DisplayName("性别")]
 7: public string Gender { get; set; }
 8: 
 9: [DisplayName("年龄")]
 10: public int? Age { get; set; }
 11: }

接下来我们定义了如下一个HomeController。在针对GET请求的Action方法Index中,我们创建了一个Person对象并将其作为Model呈现在对应的View中。另一个支持POST请求的Index方法具有一个Person类型的参数,我们在该Action方法中先调用Validate方法对这个输入参数实施验证。如果验证成功(ModeState.IsValid属性返回True),我们返回一个内容为“输入数据通过验证”的ContentResult,否则将此参数作为Model呈现在对应的View中。

 1: public class HomeController : Controller
 2: {
 3: [HttpGet]
 4: public ActionResult Index()
 5: {
 6: return View(new Person());
 7: }
 8: 
 9: [HttpPost]
 10: public ActionResult Index(Person person)
 11: {
 12: Validate(person);
 13: 
 14: if (!ModelState.IsValid)
 15: {
 16: return View(person);
 17: }
 18: else
 19: {
 20: return Content("输入数据通过验证");
 21: }
 22: }
 23: 
 24: private void Validate(Person person)
 25: {
 26: if (string.IsNullOrEmpty(person.Name))
 27: {
 28: ModelState.AddModelError("Name", "'Name'是必需字段");
 29: }
 30: 
 31: if (string.IsNullOrEmpty(person.Gender))
 32: {
 33: ModelState.AddModelError("Gender", "'Gender'是必需字段");
 34: }
 35: else if (!new string[] { "M", "F" }.Any(g => string.Compare(person.Gender, g, true) == 0))
 36: {
 37: ModelState.AddModelError("Gender", "有效'Gender'必须是'M','F'之一");
 38: }
 39: 
 40: if (null == person.Age)
 41: {
 42: ModelState.AddModelError("Age", "'Age'是必需字段");
 43: }
 44: else if (person.Age > 25 || person.Age < 18)
 45: {
 46: ModelState.AddModelError("Age", "有效'Age'必须在18到25周岁之间");
 47: }
 48: }
 49: }

如上面的代码片断所示,我们在Validate该方法中我们对作为参数的Person对象的3个属性进行逐条验证,如果提供的数据没有通过验证,我们会调用当前ModelState的AddModelError方法将指定的验证错误消息转换为ModelError保存起来。我们采用的具体的验证规则如下。

  • Person对象的Name、Gender和Age属性均为必需字段,不能为Null(或者空字符串)。
  • 表示性别的Gender属性的值必需是“M”(Male)或者“F”(Female),其余的均为无效值。
  • Age属性表示的年龄必须在18到25周岁之间。

Action方法Index对应View的定义,这是一个Model类型为Person的强类型View,它包含一个用于编辑人员信息的表单。我们直接调用HtmlHelper<TModel> 的扩展方法EditorForModel将作为Model的Person对象以编辑模式呈现在表单之中。

 1: @model Person
 2: <html>
 3: <head>
 4: <title>编辑人员信息</title>
 5: </head>
 6: <body>
 7: @Html.ValidationSummary()
 8: @using (Html.BeginForm())
 9: { 
 10: <div>@Html.LabelFor(m=>m.Name)</div>
 11: <div>@Html.EditorFor(m=>m.Name)</div>
 12: 
 13: <div>@Html.LabelFor(m=>m.Gender)</div>
 14: <div>@Html.EditorFor(m => m.Gender)</div>
 15: 
 16: <div>@Html.LabelFor(m=>m.Age)</div>
 17: <div>@Html.EditorFor(m => m.Age)</div>
 18: 
 19: <input type="submit" value="保存"/>
 20: }
 21: </body>
 22: </html>

直接运行该程序后,一个用于编辑人员基本信息的页面会被呈现出来,如果我们在输入不合法的数据并提交后,相应的验证信息会以图1所示的形式呈现出来。

二、使用ValidationAttribute特性

在大部分情况下,同一个数据类型在不同的应用场景中具有相同的验证规则,如果我们能将验证规则与数据类型关联在一起,让框架本身来实施数据验证,那么最终的开发者就可以将关注点更多地放在业务逻辑的实现上面。

比如上面演示实例中针对Person对象的验证中,我们要求Gender属性指定的表示性别的值必须是“M/m”和“F/f”两者之一,这样的验证就不得不通过自定义的ValidationAttribute特性来实现。

针对 “某个值必须在指定的范围内”这样的验证规则,我们定义一个DomainAttribute特性。如下面的代码片断所示,DomainAttribute具有一个IEnumerable<string>类型的只读属性Values提供了一个有效值列表,该列表在构造函数中被初始化。具体的验证实现在重写的IsValid方法中,如果被验证的值在这个列表中,则视为验证成功并返回True。为了提供一个友好的错误消息,我们重写了方法FormatErrorMessage。

 1: [AttributeUsage(AttributeTargets.Property | AttributeTargets.Field, AllowMultiple = false)]
 2: public class DomainAttribute : ValidationAttribute
 3: {
 4: public IEnumerable<string> Values { get; private set; }
 5: 
 6: public DomainAttribute(string value)
 7: {
 8: this.Values = new string[] { value };
 9: }
 10: 
 11: public DomainAttribute(prams string[] values)
 12: {
 13: this.Values = values;
 14: }
 15: 
 16: public override bool IsValid(object value)
 17: {
 18: if (null == value)
 19: {
 20: return true;
 21: }
 22: return this.Values.Any(item => value.ToString() == item);
 23: }
 24: 
 25: public override string FormatErrorMessage(string name)
 26: {
 27: string[] values = this.Values.Select(value => string.Format("'{0}'",value)).ToArray();
 28: return string.Format(base.ErrorMessageString, name,string.Join(",", values));
 29: }
 30: }

由于ASP.NET MVC在进行参数绑定的时候会自动提取应用在目标参数类型或者数据成员上的ValidationAttribute特性,并利用它们对提供的数据实施验证,所以我们不再需要像上面演示的实例一样自行在Action方法中实施验证,而只需要在定义参数类型Person的时候应用相应的ValidationAttribute特性将采用的验证规则与对应的数据成员相关联。如下所示的是属性成员上应用了相关ValidationAttribute特性的Person类型的定义。我们在三个属性上均应用了RequiredAttribute特性将它们定义成必需的数据成员,Gender和Age属性上则分别应用了DomainAttribute和RangeAttribute特性对有效属性值的范围作了相应限制。

 1: public class Person
 2: {
 3: [DisplayName("姓名")]
 4: [Required(ErrorMessageResourceName = "Required", ErrorMessageResourceType = typeof(Resources))]
 5: public string Name { get; set; }
 6: 
 7: [DisplayName("性别")]
 8: [Required(ErrorMessageResourceName = "Required", ErrorMessageResourceType = typeof(Resources))]
 9: [Domain("M", "F", "m", "f", ErrorMessageResourceName = "Domain", ErrorMessageResourceType = typeof(Resources))]
 10: public string Gender { get; set; }
 11: 
 12: [DisplayName("年龄")]
 13: [Required(ErrorMessageResourceName = "Required", ErrorMessageResourceType = typeof(Resources))]
 14: [Range(18, 25, ErrorMessageResourceName = "Range", ErrorMessageResourceType = typeof(Resources))]
 15: public int? Age { get; set; }
 16: }

三个ValidationAttribute特性采用的错误消息均定义在项目默认的资源文件中(我们可以采用这样的步骤创建这个资源文件:右键选择Solution Exploror中的项目,并在上下文菜单中选择“属性”选项打开“项目属性”对象框。最后在对话框中选择“资源”Tab页面,通过点击页面中的链接创建一个资源文件),具体定义如图2所示。

三、让数据类型实现IValidatableObject接口

除了将验证规则通过ValidationAttribute特性直接定义在数据类型上并让ASP.NET MVC在进行参数绑定过程中据此来验证参数之外,我们还可以将验证操作直接定义在数据类型中。既然我们将验证操作直接实现在了数据类型上,意味着对应的数据对象具有“自我验证”的能力,我们姑且将这些数据类型称为“自我验证类型”。这些自我验证类型是实现了具有如下定义的接口IValidatableObject,该接口定义在“System.ComponentModel.DataAnnotations”命名空间下。

 1: public interface IValidatableObject
 2: {
 3: IEnumerable<ValidationResult> Validate(ValidationContext validationContext);
 4: }

如上面的代码片断所示,IValidatableObject接口具有唯一的方法Validate,针对自身的验证就实现在该方法中。对于上面演示实例中定义的数据类型Person,我们可以按照如下的形式将它定义成自我验证类型。

 1: public class Person: IValidatableObject
 2: {
 3: [DisplayName("姓名")]
 4: public string Name { get; set; }
 5: 
 6: [DisplayName("性别")]
 7: public string Gender { get; set; }
 8: 
 9: [DisplayName("年龄")]
 10: public int? Age { get; set; }
 11: 
 12: public IEnumerable<ValidationResult> Validate(ValidationContext validationContext)
 13: {
 14: Person person = validationContext.ObjectInstance as Person;
 15: if (null == person)
 16: {
 17: yield break;
 18: }
 19: if(string.IsNullOrEmpty(person.Name))
 20: {
 21: yield return new ValidationResult("'Name'是必需字段", new string[]{"Name"});
 22: }
 23: 
 24: if (string.IsNullOrEmpty(person.Gender))
 25: {
 26: yield return new ValidationResult("'Gender'是必需字段", new string[] { "Gender" });
 27: }
 28: else if (!new string[]{"M","F"}.Any(g=>string.Compare(person.Gender,g, true) == 0))
 29: {
 30: yield return new ValidationResult("有效'Gender'必须是'M','F'之一", new string[] { "Gender" });
 31: }
 32: 
 33: if (null == person.Age)
 34: {
 35: yield return new ValidationResult("'Age'是必需字段", new string[] { "Age" });
 36: }
 37: else if (person.Age > 25 || person.Age < 18)
 38: {
 39: yield return new ValidationResult("'Age'必须在18到25周岁之间", new string[] { "Age" });
 40: } 
 41: }
 42: }

如上面的代码片断所示,我们让Person类型实现了IValidatableObject接口。在实现的Validate方法中,我们从验证上下文中获取被验证的Person对象,并对其属性成员进行逐个验证。如果数据成员没有通过验证,我们通过一个ValidationResult对象封装错误消息和数据成员名称(属性名),该方法最终返回的是一个元素类型为ValidationResult的集合。在不对其他代码作任何改动的情况下,我们直接运行该程序并在输入不合法数据的情况下提交表单后依然会得到如图1所示的输出结果。

四、让数据类型实现IDataErrorInfo接口

上面我们让数据类型实现IValidatableObject接口并将具体的验证逻辑定义在实现的Validate方法中,这样的类型能够被ASP.NET MVC所识别,后者会自动调用该方法对绑定的数据对象实施验证。如果我们让数据类型实现IDataErrorInfo接口也能实现类似的自动化验证效果。

IDataErrorInfo接口定义在“System.ComponentModel”命名空间下,它提供了一种标准的错误信息定制方式。如下面的代码片段所示,IDataErrorInfo具有两个成员,只读属性Error用于获取基于自身的错误消息,而只读索引用于返回指定数据成员的错误消息。

 1: public interface IDataErrorInfo
 2: {
 3: string Error { get; }
 4: string this[string columnName] { get; }
 5: }

同样是针对上面演示的实例,现在我们对需要被验证的数据类型Person进行了重新定义。如下面的代码片断所示,我们让Person实现了IDataErrorInfo接口。在实现的索引中,我们将索引参数columnName视为属性名称,根据它按照上面的规则对相应的属性成员实施验证,并在验证失败的情况下返回相应的错误消息。在不对其他代码作任何改动的情况下,我们直接运行该程序并在输入不合法数据的情况下提交表单后依然会得到如图1所示的输出结果。

github上很多著名的项目都有.editorconfig文件,那么这个文件有什么作用呢?


dotnet/wpf

dotnet/aspnetcore

在现代软件开发中,团队协作和代码维护是至关重要的。为了确保代码的可读性和一致性,开发团队需要共享相同的代码风格和规范。然而,手动调整每个开发人员的编辑器设置可能非常耗时且容易出错。在这种情况下,.editorconfig 配置文件可以发挥作用。本文将详细介绍.editorconfig文件的用途、语法和常见配置选项,以帮助你更好地管理团队的代码风格。

什么是.editorconfig文件

.editorconfig 是一个纯文本文件,用于定义项目中的代码风格和编辑器行为。它提供了一种简单的方式,通过一致的方式应用代码格式设置,确保所有团队成员在不同的编辑器和开发环境中看到一致的代码样式。.editorconfig 文件通过基于规则的语法指定各种设置,例如缩进、字符编码、换行符类型、文件格式等。

.editorconfig文件的语法和配置选项

.editorconfig 文件使用类似于 INI 文件的格式,其中包含一系列的节(section)和键值对(key-value pairs).以下是.editorconfig文件的语法和常见的配置选项

  1. 文件匹配规则

使用"[]"指定适用于所有文件,或使用特定的文件扩展名来匹配文件类型,例如"[.js]"匹配JavaScript文件。

  1. 配置选项

以下是一些常见的配置选项及其用法:

  • indent_style:设置缩进风格,可选值有"tab"(制表符)或"space"(空格)。
  • indent_size:设置每个缩进级别的空格数或制表符数目。
  • tab_width:设置制表符的宽度。
  • end_of_line:设置换行符格式,可选值有"lf"(Unix/Linux风格),"crlf"(Windows风格)或"cr"(旧版Mac风格)。
  • charset:设置文件字符集,可选值有"utf-8"、"utf-8-bom"、"utf-16le"、"utf-16be"或"latin1"。
  • trim_trailing_whitespace:设置是否删除行末尾的空白字符,值为"true"或"false"。
  • insert_final_newline:设置是否在文件末尾插入最后一行的空行,值为"true"或"false"。
  • root:标识.editorconfig文件的起始位置,用于覆盖父级文件夹的配置。
  1. 示例

以下是一个.editorconfig文件的示例:

Copy Code# 设置通用的缩进风格和大小为4
[*]
indent_style = space
indent_size = 4

# 设置JavaScript特定的配置
[*.js]
indent_style = space
indent_size = 2

在上述示例中,所有文件都使用空格缩进,缩进大小为4个空格。而JavaScript文件(扩展名为.js)使用2个空格缩进。

使用.editorconfig文件

很多编辑器和开发工具都支持.editorconfig 文件。在你的项目中创建一个 `.editorconfig` 文件,并将其放置在根目录下。保存后,编辑器将自动识别并应用相应的代码风格设置。

它可以应用于各种软件开发项目,包括 C#、Java、Python、JavaScript 等。无论你使用哪种语言,都可以通过 .editorconfig 文件来确保团队成员遵循统一的代码风格和编码规范。
.editorconfig 文件提供了一种简单而有效的方式,以一致的方式配置和应用代码风格设置。通过使用该文件,团队可以避免手动调整每个开发人员的编辑器设置,提高代码的可读性和一致性。这种统一的代码风格有助于增强团队协作、减少错误,并且提高代码的可维护性。

.editorconfig文件可视化编辑

在 Visual Studio 2022 中,.editorconfig文件的配置选项可以按照以下四个类别进行分组和设置:空格(Whitespace),代码样式(Code Style),命名样式(Naming Style)和分析器(Analyzer)。下面对这四个类别进行详细介绍:

1. 空格(Whitespace)类别:

- indent_style:设置缩进风格,可选值有"tab"(制表符)或"space"(空格)。

- indent_size:设置每个缩进级别的空格数目。

- tab_width:设置制表符的宽度。

- trim_trailing_whitespace:设置是否删除行末尾的空白字符。

2. 代码样式(Code Style)类别:

- braces_on_new_line:设置大括号是否单独占一行。

- space_before_parentheses:设置是否在函数调用、控制结构和类型转换中的括号前加空格。

- space_within_parentheses:设置是否在括号内部添加空格。

- space_around_operators:设置是否在运算符周围添加空格。

- new_line_before_else:设置是否在if语句后的else语句之前添加空行。

3. 命名样式(Naming Style)类别:

- dotnet_naming_rule:定义用于检查标识符名称的命名规则。

- dotnet_naming_symbols:指定要应用命名规则的符号类型和范围。

4. 分析器(Analyzer)类别:

- dotnet_diagnostic.[规则名称].severity:设置指定分析规则的严重程度。

- dotnet_diagnostic.[规则名称].style:设置指定分析规则的样式。

这些分类使得在.editorconfig文件中可以更清晰地组织和管理不同类型的配置选项,以便控制代码的空格使用、代码样式、命名约定以及应用分析规则的方式。通过对这些分类进行适当配置,可以确保整个团队在项目中使用一致的编码规范和代码质量标准。

#头条创作挑战赛#