2011年12月13日 星期二

ASP.NET MVC 3 使用 CKEditor

以往在開發 ASP.NET WebForm 專案時,如果專案需求是有編輯 HTML 圖文資料時,大多都會選擇使用 FCKEditor,FCKEditor 是個相當不錯的 HTML 編輯器,能夠符合許多情境的需求,然而 FCKEditor 的開發已經停止,轉而開發 CKEditor,CKEditor 是將原本的 FCKEditor 給全部重新寫過,雖然大致上的設定還是與原本的FCKEditor 相去不遠,但卻有著很大的不同,我在 ASP.NET MVC 3 專案上還沒有對 CKEditor 做過整合,所以此次就練習在 ASP.NET MVC 3 專案上整合 CKEditor 的功能,使用 NuGet 來取得 CKEditor,並且試著用不同的方式在前端與後端的傳值上做些不同的變化。



CKEditor

目前版本:3.6.2

網址:http://ckeditor.com/

image

Demo:http://ckeditor.com/demo

Download:http://ckeditor.com/download

Documention:http://docs.cksource.com/

接下來這個連結是我認為比較重要的,很多的設定與CKEditor的重要資訊都可以在裡面找到,

CKEditor 3 JavaScript API Documentation

http://docs.cksource.com/ckeditor_api/index.html

在Download的最下方還是有提供FCKEditor最後版本的下載,不過還特別註明:retired (退休啦~)

image



一開始我們就說過,我們要在Visual Studio中使用NuGet來安裝CkEditor,所以就先別急著在官網上面去把最新的版本給下載下來,以下是CKEditor在NuGet Gallery的相關資訊。

NuGet Gallery - CKEditor 3.6.2

http://nuget.org/packages/CKEditor

PM> Install-Package CKEditor

image

 


於新的ASP.NET MVC 3網站中加入CKEditor

直接再VS2010中新增一個空白的ASP.NET MVC3網站專案

SNAGHTML7ffa48

選擇Razor的ViewEngine(如果不熟悉的話也可以切換為ASPX的ViewEngine)

SNAGHTML809973

按下確定之後就會建立好一個「空空」的網站內容,真的是空的…

image

 

這時候還不用急著去建立新的Controller、View等,要先做的就是開啟NuGet Manage對專案中的套件、組件、函式庫做升級的動作。

SNAGHTML871325

 

上述的升級動作都做完之後,接下來就是切換到Manage NuGet Package視窗的「Online」並且在搜尋列輸入「CKEditor」

SNAGHTML8c0408

 

在上面的所搜尋出來的CKEditor中按下「Install」就可以將CKEditor完整安裝到網站中,安裝的過程需要一段時間,因為會把所有CKEditor會用到的JS檔案、圖檔等都一一下載到網站專案裡。

SNAGHTML8dd461

 

安裝完成後,會在Scripts目錄中看到完成安裝的CKEditor

image

 


使用 CKEditor

安裝好 CKEditor 之後就是要來使用它啦…至於中間過程的建立 Controller 與 ViewPage 的步驟就跳過,直接進入到使用的步驟來說明。

首先比較重要的是,先在 Views/Shared 目錄下的 _Layout.cshtml 去加入「Scripts/ckeditor/ckeditor.js」,像我的話,我都會把 Include Javascript 的部份給抽出來另外建立為一個部分檢視,然後也會在 _Layoout.cshtml 去加入一個RenderSection,用來放置每個ViewPage所要建立的Javascript程式,

如下:

Views/Shared/_Layout.cshtml

<!DOCTYPE html>
<html>
<head>
    <title>@ViewBag.Title</title>
    <link href="@Url.Content("~/Content/Site.css")" rel="stylesheet" type="text/css" />
</head>
<body>
    @RenderBody()
  @Html.Partial("IncludeJavascript")
  @RenderSection("JavascriptContent", required: true)
</body>
</html>

Views/Shared/IncludeJavascript.cshtml

<script src="@Url.Content("~/Scripts/jquery-1.7.1.min.js")" type="text/javascript"></script>
<script src="@Url.Content("~/Scripts/ckeditor/ckeditor.js")" type="text/javascript"></script>

 

接著下來,建立一個Craeate.cshtml,這個ViewPage是呈現一個簡單的文章編輯頁面,而文章內容就是要使用CKEditor

@model Test.Web.ODP.Models.Article
@{
  ViewBag.Title = "Create";
  Layout = "~/Views/Shared/_Layout.cshtml";
}
<h2>Create</h2>
@using (Html.BeginForm("Create", "Home", FormMethod.Post, new { id = "FormCreate" }))
{
  <fieldset>
    <legend>Article</legend>
    <div class="editor-label">
      @Html.LabelFor(model => model.SUBJECT)
    </div>
    <div class="editor-field">
      @Html.TextBox("subject", null, new { id = "subject", style = "width: 400px", MaxLength = "100" })
    </div>
    <div class="editor-label">
      @Html.LabelFor(model => model.CONTENT)
    </div>
    <div class="editor-field">
      @Html.TextArea("content", new { id = "content", @name = "content" })
    </div>
    <p>
      <input type="button" value="Create" id="ButtonCreate" />
    </p>
  </fieldset>
   
}
<div>
  @Html.ActionLink("Back to List", "Index")
</div>

其中「@Html.TextArea("content", new { id = "content", @name = "content" })」建立一個 TextArea,而這個 TextArea 就是 CKEditor 要取代的 Html Element,

 

再來就是要在Javascript的區域中去做宣告,讓這個TexrArea置換為CKEditor

<script type="text/javascript" language="javascript">
var editor = CKEDITOR.editor.replace('content', { skin: 'kama', width: '800px' });
</script>

CKEDITOR.editor.replace()方法

第一個參數就是要指定取代的TextArea Name,

第二個參數則是設定這個CKEditor的外觀樣式,如外觀樣式、高度、寬度等,

有關 skin 的外觀樣式,可以開啟 Scripts/ckeditor/skins 去找到外觀樣式的名稱,安裝後預設有 Kama, office2003, v2

image

 

設定好之後,就可以來看看這個頁面的執行結果,

image



把 CKEditor 裡面的內容傳送到 Server 端

在 CKEditor 所編輯好的內容當然還是要送回到Server端去做儲存的處理,其中最簡單的方式就是直接用Form Post傳送,要用Form Post 傳送到Server端,當然就是要先在ViewPage中去usgin Hrml.BeginForm,如下:

image

而這個Form所要使用的Action網址就是Controller中的Create Action方法,在ASP.NET WebForm開發,當遇到前端要傳送含有HTML Tag的內容時,如果沒有在該頁面去設定 ValidateInput = false,那一定會出現警告訊息,同理,在ASP.NET MVC也是一樣,但是ValidateInput的設定就不是在ViewPage上面去做設定,而且可以去設定的地方有兩個地方。

先來看看如果不去設定ValidateInput時會看到的畫面:

image

 

設定的地方一:

Controller 中所對應的Action方法上,使用Attribute「ValudateInput」,除了使用HttpPost之外,要設定 ValidateInput(false)

image

而在這邊一定要特別的強調,因為我們將ValidateInput給設定為false,無疑就是開了一道安全大門給有心破壞的人來擅闖,所以為了避免網站遭受到XSS攻擊或是輸入的內容去帶有不友善或是惡意的指令碼,這個時候我們一定要再加上一道安全的鎖,

Microsoft Web Protection Library – AntiXSS Library V4.0

Codeplex:http://wpl.codeplex.com/

Download:ww.microsoft.com/download/en/details.aspx?id=5242

NuGet:https://nuget.org/packages/AntiXSS

於VS2010就可以透過NuGet幫網站專案安裝上 AntiXSS

SNAGHTMLe6d388

相關介紹與使用方法,請各位參閱網路上的文件與資料。

程式中使用AntiXSS的 Sanitizer.GetSageHtmlFragement() 方法,取得安全的HTML區段內容。

image

在Controller的Create Action方法加上 [Validate(false)] Attribute 後就可以前端傳送帶有HTML Tag內容的資料道Server端,請記得一定要讓網站加上AntiXSS並且使用,以加強防護。

 

設定的地方二:

除了在Action方法加上 [Validate(false)] Attribute 外,另外也還有個地方可以設定,在 Scripts/ckeditor 目錄下找個檔案「config.js

檔案內容:

image

config.js 可以對CKEditor做很多的設定,不過這些並不在此次的說明範圍中,在這個config.js檔案中可以加上「 config.htmlEncodeOutput = true; 」,修改如下:

image

修改好 config.js 後,為了驗證這裡的修改,我們把原本加在 HomeController Create Action方法前的Attribute拿掉,

image

接著就執行程式來看看執行的狀況:

不會在出現黃頁的警告訊息,而且有可以順利執行 Action方法

image

所以可以確定在 ckeditor/config.js 加上config.htmlEncodeOutput = true 的狀況下,Controller的Action方法是可以不用加上 [Validate(false)] Attribute ,不過咧~ 最好的作法還是不論前後端,都各自加上設定,也加上 AntiXSS的防護。

 

回到傳送資料到後端的部份,前面說到最直接的方式就是用Form Post的方式,但有時會為了不要讓整頁做POST的動作而想要用AJAX的方法將鎖需要傳送到後端的值給POST出去,這時候就必須要在前端先把FCKEditor的內容給取出來,再使用jQuery的AJAX方法然後與其他資料一併送到Server端。

 

前端使用jQuery AJAX方法傳送資料到Server端 - 1

前端的Javascript的程式中要去取得CKEditor的內容,不能直接以下的方法來取得:

var content = $('#content').val();

這樣是取不到內容的,因為TextArea content已經在之前用以下的程式給置換為CKEditor:

var editor = CKEDITOR.editor.replace('content', { skin: 'kama', width: '800px' });

所以應該是需要用下列的方式來取得CKEditor的內容:

var content = editor.getData();

那麼前端的完整程式如下:

function CreateArticle()
{
  var subject = $.trim($('#subject').val());
  var content = editor.getData();
  if (subject.length == 0)
  {
    alert('請輸入標題');
    return false;
  }
  if (content.length == 0)
  {
    alert('請輸入內容');
    return false;
  }
  $.ajax(
  {
    url: '@Url.Action("Create", "Home")',
    type: 'post',
    data:  { subject: subject, content: content },
    cache: false,
    async: false,
    dataType: 'json',
    success: function (data)
    {
      if (data.Msg)
      {
        alert(data.Msg);
        return false;
      }
      else
      {
        if (data.Result == 'Success')
        {
          alert('Success');
          location.href = '@Url.Action("Index", "Home")';
        }
        else
        {
          alert(data.ResultMessage);
          return false;
        }
      }
    }
  });
}

透過上面的程式處理就可以把指定的資料給傳送到Server端,不過這樣的處理方式會比較適合網頁表單的欄位比較少的情況下,哪萬一欄位變得很多,用一個一個取值然後放值的動作就會顯得很費力也相當不聰明,不想透過Form-post的方式又要處理很多個表單欄位資料時,jQuery還有另外兩個方法,一個是 serialize() 另一個是 serializeArray().

 

前端使用jQuery AJAX方法傳送資料到Server端 - 2 使用 serialize()

先來看看使用使用 serialize() 來取得 FormCreate表單下的欄位資料內容,

var data = $('#FormCreate').serialize();
console.log(data);

取得的資料:

image

可以看到content是取不到資料的,此時可以用以下的取值方式取得資料後再把資料給加入到serialize()的資料中,

var content = editor.getData();

不過 $(‘#FormCreate’).serialize(); 所取得的資料是字串,要再將CKEditor的內容給串接到一個字串中,並不是很理想。

 

前端使用jQuery AJAX方法傳送資料到Server端 - 3 使用 serializeArray()

以serializeArray()所取得表單資料:

image

使用serializeArray()所取得的資料會以Name-Value的方式處理為物件陣列,如此一來會比直接用字串的串接處理更好,因為是陣列的資料,我們可以使用我之前所介紹的Linq to Javascript (JSLINQ)來處理這個陣列,並且把 editor.getData()的資料給放到陣列中 name = “content” 的物件value中,如下:

var values = $("#FormCreate").serializeArray();
new JSLINQ(values).Where(function (item)
{
  if (item["name"] == "content" && item["value"].length == 0)
  {
    item["value"] = editor.getData();
  }
});

最後在jQuery.AJAX方法中,data還是要以字串的方式傳送出去,這個時候可以使用jQuery.Param()方法來處理陣列的資料,

jQuery.param(values),

以下是完整的前端處理程式:

function CreateArticle()
{
  var subject = $.trim($('#subject').val());
  var content = editor.getData();
  if (subject.length == 0)
  {
    alert('請輸入標題');
    return false;
  }
  if (content.length == 0)
  {
    alert('請輸入內容');
    return false;
  }
  var values = $("#FormCreate").serializeArray();
  new JSLINQ(values).Where(function (item)
  {
    if (item["name"] == "content" && item["value"].length == 0)
    {
      item["value"] = editor.getData();
    }
  });
  $.ajax(
  {
    url: '@Url.Action("Create", "Home")',
    type: 'post',
    data: jQuery.param(values),
    cache: false,
    async: false,
    dataType: 'json',
    success: function (data)
    {
      if (data.Msg)
      {
        alert(data.Msg);
        return false;
      }
      else
      {
        if (data.Result == 'Success')
        {
          alert('Success');
          location.href = '@Url.Action("Index", "Home")';
        }
        else
        {
          alert(data.ResultMessage);
          return false;
        }
      }
    }
  });
}

 

另外也提供Server端的 HomeController Create() Action程式內容:

[HttpPost]
[ValidateInput(false)]
public ActionResult Create(string subject, string content)
{
  Dictionary<string, string> jo = new Dictionary<string, string>();
  
  if (string.IsNullOrWhiteSpace(subject))
  {
    jo.Add("Msg", "沒有輸入標題");
    return Content(JsonConvert.SerializeObject(jo), "application/json");
  }
  if (string.IsNullOrWhiteSpace(content))
  {
    jo.Add("Msg", "沒有輸入內容");
    return Content(JsonConvert.SerializeObject(jo), "application/json");
  }
  try
  {
    Article article = new Article();
    article.SUBJECT = Sanitizer.GetSafeHtmlFragment(subject);
    article.CONTENT = Sanitizer.GetSafeHtmlFragment(content);
    articleService.Create(article);
    jo.Add("Result", "Success");
  }
  catch (Exception ex)
  {
    jo.Add("Result", "Failure");
    jo.Add("ResultMessage", ex.Message);
  }
  return Content(JsonConvert.SerializeObject(jo), "application/json");
}
public ActionResult Details(string id)
{
  if (string.IsNullOrWhiteSpace(id))
  {
    return RedirectToAction("Index", "Home");
  }
  Article article = articleService.FindOne(id);
  return View(article);
}
public ActionResult Edit(string id)
{
  return View();
}
}

 


參考連結:

demoshop - ASP.NET MVC validateRequest 取消驗證失效?

KKBruce - ASP.NET MVC - HTML 編輯器 | (3) CKEditor + CKFinder

 

CKEditor 3 JavaScript API Documentation

Class CKEDITOR.editor - getData()
Namespace CKEDITOR.config - CKEDITOR.config.htmlEncodeOutput

jQuery

.serialize()
.serializeArray()
jQuery.param()

 

以上

3 則留言:

  1. 請問我ckediter建立好以後,假如編輯欄位新增123存入資料庫,在打開就會出現//p 123 //p所以一直編輯一直就會增加
    編輯欄位就會出現//p <p> 123</p> //p,一直編輯就會越來越多,是我哪裡設定不對嗎?

    回覆刪除
    回覆
    1. Hello,
      如果只是在 config.js 加上 config.htmlEncodeOutput = true 的話
      這是把輸入的內容以 Html Encode 做轉換然後傳送當後端,
      所以需要再做編輯的時候,需要將存到資料庫的內容作轉換(使用 HttpUtility.HtmlDecode 方法) 後再輸出到 View.
      抱歉,因為這是兩年前的文章了, 所以當初文章裡就沒有再對這個部分做詳細說明.

      刪除
    2. 感謝你非常用心的解決問題,還特定開了新的一篇文章,我學到了~

      刪除

提醒

千萬不要使用 Google Talk (Hangouts) 或 Facebook 及時通訊與我聯繫、提問,因為會掉訊息甚至我是過了好幾天之後才發現到你曾經傳給我訊息過,請多多使用「詢問與建議」(在左邊,就在左邊),另外比較深入的問題討論,或是有牽涉到你實作程式碼的內容,不適合在留言板裡留言討論,請務必使用「詢問與建議」功能(可以夾帶檔案),謝謝。