整合營銷服務商

          電腦端+手機端+微信端=數據同步管理

          免費咨詢熱線:

          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文件中可以更清晰地組織和管理不同類型的配置選項,以便控制代碼的空格使用、代碼樣式、命名約定以及應用分析規則的方式。通過對這些分類進行適當配置,可以確保整個團隊在項目中使用一致的編碼規范和代碼質量標準。

          #頭條創作挑戰賽#


          主站蜘蛛池模板: 99无码人妻一区二区三区免费| 无码中文字幕一区二区三区| 无码精品国产一区二区三区免费| 国产精品538一区二区在线| 乱码人妻一区二区三区| 在线视频亚洲一区| 午夜福利一区二区三区在线观看| 亚洲一区精品无码| 欧美亚洲精品一区二区| 中文字幕一区二区视频| 精品一区二区久久久久久久网精 | 无码人妻精品一区二区三区久久| 日韩精品电影一区亚洲| 毛片无码一区二区三区a片视频| 一区二区三区视频| 精品无人区一区二区三区| 日韩久久精品一区二区三区 | 久久国产午夜精品一区二区三区 | 性色AV 一区二区三区| 亚洲av乱码一区二区三区按摩| 无码人妻精品一区二区三区东京热 | 国产综合无码一区二区三区| 国产一区美女视频| 中文字幕无线码一区| 亚洲国产高清在线一区二区三区 | 日本一道高清一区二区三区| 久久久久人妻精品一区三寸蜜桃 | 立川理惠在线播放一区| 午夜视频在线观看一区二区| 鲁大师成人一区二区三区 | 国产福利一区二区| 无码AV一区二区三区无码 | eeuss鲁片一区二区三区| 亚洲国产精品一区二区成人片国内 | 在线电影一区二区| 国产日韩综合一区二区性色AV| 久久se精品一区精品二区国产 | 69福利视频一区二区| 内射一区二区精品视频在线观看| 国产一区二区在线观看app| 国产一区二区在线|