本文章是由機器翻譯。

資料點

為 10 年的 ASP.NET Web Form 應用程式注入新生命

Julie Lerman

下載代碼示例

舊版程式碼:不能與它一起生活,不能沒有它。更好的工作你做同一個應用程式,它將閒逛的時間越長。我第一次的ASP.NETWeb 表單應用程式已使用了 10 年多一點。它是最後越來越被替換其他人正在編寫一個平板電腦應用程式。然而,在此期間,用戶端要求我向它添加一個新的功能將會讓公司開始馬上收集的一些資料的新版本將聚集。

這不是一個簡單的欄位或兩個的問題。在現有應用程式中 — — 用於跟蹤員工工時的時間張複雜 — — 那裡是一個動態集定義的任務的工作清單的核取方塊。用戶端維護該清單中單獨的應用程式。在 Web 應用程式中,使用者可以檢查項目的清單,指定他執行的任務的任何數目。清單中包含略多於 100 個專案,並且隨著時間的推移慢慢地增長。

現在,用戶端想要跟蹤的每個所選的任務上花費的小時數。這款應用程式將用於僅幾個月,所以是沒有任何意義,很多投資入它,卻有關更改的兩個重要目標:

  1. 可以對使用者輸入的時間,這意味著不需要任何額外的按鈕按一下或導致回發真的很輕鬆。
  2. 在可能的最少創方式向代碼中添加該功能。雖然它會忍大修 10 歲 app 用更現代的工具,我想不會影響現有的 (工作) 代碼,包括資料訪問和資料庫的方式添加新的邏輯。

我花了一些時間考慮我的選擇。目標號2 意味著使 CheckBoxList 保持不變。我決定要包含在一個單獨的網格,但目標號小時1 意味著不使用ASP.NETGridView 控制項 (謝天謝地)。我決定使用一個表和 JavaScript 用於檢索和堅持任務小時資料,探討了幾種方法可以實現這一目標。不能使用 AJAX PageMethods 調用代碼隱藏,因為我的頁面被使用 Server.Transfer 從另一網頁檢索。內聯調用,如<%mycodebehindmethod()%>,一直工作到我不得不做一些複雜的資料驗證 (太難在 JavaScript 中完成),所需的用戶端和伺服器端物件混合。這種情況也開始變醜與需要盡一切的內聯調用靜態的感動。所以我未能在"至少創"。

最後,我意識到真的應該保持的新邏輯完全分離,並把它放入將很容易訪問到從 JavaScript WebAPI。這將有助於保持清潔分離的新邏輯和舊之間。

我仍挑戰。我事先實驗­rience 與 Web API 是要創建一個新的 MVC 專案。我開始的但從現有的應用程式調用 Web API 中的方法造成違抗我能找到避免 CORS 每個模式的交叉起源資源分享 (CORS) 問題的原因。最後,我發現一篇文章由邁克華士信有關直接進入 Web 表單專案添加 Web API (bit.ly/1jNZKzI) 和我正要 — — 雖然我有很多橋樑尚未以交叉。我不會讓你重溫我的痛苦我猛打,捶打,摸索著我對成功的方式。相反,我送你通過我最終達成的解決辦法。

而不是目前我的客戶真正的應用程式,以演示如何我帶入了舊應用程式的新功能,我將使用一個示例,它跟蹤使用者的首選項通過他們喜歡做的事情的評論:有趣的東西。我會放棄 100 多項 ; 清單中 該表單顯示只有短 CheckBoxList,以及資料驗證的額外工作。跟蹤工時,不會跟蹤使用者評論。

一次我致力於 Web API,添加驗證方法根本就不一項挑戰。因為我創造了新的示例,我使用了 Microsoft.NET 框架 4.5 和Entity Framework6 (EF) 而不是.NET Framework 2.0 和原料ADO.NET。圖 1 顯示了應用程式範例的起始點:與使用者的名稱和可能的活動可編輯 CheckBoxListASP.NETWeb 表單。這是我會向其中添加的能力來跟蹤每個選中的項的注釋,如圖所示的速寫在網格的頁。


圖 1 起點:加上計畫簡單ASP.NETWeb 表單

쭮힗 1좬添加新的類

我需要一個類來存儲新的注釋。鑒於我的資料,它取得了要使用的使用者 Id 和 FunStuffId 組成一個鍵來確定注釋會附加到哪個使用者和有趣的活動最有意義的結論:

namespace DomainTypes{
  public class FunStuffComment{
    [Key, Column(Order = 0)]
    public int UserId { get; set; }
    [Key, Column(Order = 1)]
    public int FunStuffId { get; set; }
    public string FunStuffName { get; set; }
    public string Comment { get; set; }
  }
}

因為我計畫要使用 EF 保持這些資料,我需要指定的屬性,將會成為我複合鍵。在 EF,映射複合鍵的關鍵就是要添加的列順序屬性以及 Key 屬性。我還想指出的 FunStuffName 屬性。即使我可以交叉引用我 FunStuff 表,以獲取特定的條目的名稱,我發現它只是表面 FunStuffName 更易於在此類中。它可能看上去是多餘的但請記住我的目標,避免惹現有的邏輯。

쭮힗 2좬將 Web API 添加到基於表單的 Web 專案

由於華士信的文章,我學會了我可以直接入現有的專案添加一個 Web API 控制器。只需按右鍵解決方案資源管理器中的專案,你會看到 Web API 控制器類作為下添加內容功能表中的選項。創建的控制器被設計工作與 MVC,所以第一順序的業務是以刪除所有的方法和我的評論方法用於檢索特定使用者的現有注釋中添加。因為我會用微風 JavaScript 庫,已經安裝了它我使用 NuGet 的專案,我使用的微風命名約定對於我的 Web API 控制器類,正如你可以看到在圖 2。我還沒來評論鉤住我的資料訪問,然而,那麼我就會開始通過返回一些記憶體中的資料。

圖 2 BreezeController Web API

namespace April2014SampleWebForms{
[BreezeController] 
public class BreezeController: ApiController  {
  [HttpGet]
  public IQueryable<FunStuffComment> Comments(int userId = 0)
    if (userId == 0){ // New user
      return new List<FunStuffComment>().AsQueryable();
    }
      return new List<FunStuffComment>{
        new FunStuffComment{FunStuffName = "Bike Ride",
          Comment = "Can't wait for spring!",FunStuffId = 1,UserId = 1},
        new FunStuffComment{FunStuffName = "Play in Snow",
          Comment = "Will we ever get snow?",FunStuffId = 2,UserId = 1},
        new FunStuffComment{FunStuffName = "Ski",
          Comment = "Also depends on that snow",FunStuffId = 3,UserId = 1}
      }.AsQueryable();    }
  }
}

華士信的文章將引導您要添加路由到 global.asax 檔。但是,添加通過 NuGet 微風創建一個.config 檔的已定義將適當的路由選擇。這就是為什麼我使用的命名中的控制器中推薦的微風圖 2

現在我可以從我的 FunStuffForm 的用戶端方便地調用評論方法。我想測試我 Web API 在瀏覽器中,以確保工作的事情,你可以通過運行應用程式,然後流覽到 HTTP://localhost:1378/微風/微風/評論這樣做嗎? UserId = 1。請務必使用正確的主機: 埠正在使用您的應用程式。

쭮힗 3좬添加用戶端資料繫結

但我還沒有完成。我需要的資料,用來做些所以回過頭來看我以前的專欄上 Knockout.js (msdn.microsoft.com/magazine/jj133816 JavaScript 資料繫結) 和微風 (msdn.microsoft.com/magazine/jj863129,這使得資料繫結甚至更簡單)。微風自動將我的 Web API 的結果轉換成挖空 (和其他 Api) 可以直接使用,而不必創建附加視圖模型和映射邏輯的可綁定的物件。添加資料繫結是最密集部分的轉換,我仍然非常有限的 JavaScript 和 jQuery 技巧提出更糟。但我咬牙堅持著 — — 並且也成為了半 pro 在 JavaScript 在 Chrome 中一路走來進行調試。大部分的新代碼是在單獨的 JavaScript 檔是綁定到我原來的 Web 表單頁,FunStuffForm.aspx。

當我幾乎完這篇文章時,有人指出挖空現在有點日 ("它是如此 2012 年,"他說),和許多 JavaScript 開發人員轉而使用更簡單和更豐富的框架,如 AngularJS 或 DurandalJS。這是一個讓我學習另一天的教訓。我相信我 10 多歲的應用程式不會介意一個 2 歲的工具。但我肯定把看看這些工具在以後的專欄中。

在我的 Web 表單,定義名的評論為與填充的由我會綁定到它與挖空的資料欄位的列的表 (請參閱圖 3)。我也是綁定的使用者 Id 和 FunStuffId 的欄位,我稍後會需要,但保持他們隱藏的。

圖 3 HTML 表,為與挖空的綁定設置最多

<table id="comments">
  <thead>
    <tr>
      <th></th>
      <th></th>
      <th>Fun Stuff</th>
      <th>Comment</th>
    </tr>
  </thead>
  <tbody data-bind="foreach: comments">
    <tr>
      <td style="visibility: hidden" data-bind="text: UserId"></td>
      <td style="visibility: hidden" data-bind="text: FunStuffId"></td>
      <td data-bind="text: FunStuffName"></td>
      <td><input data-bind="value: Comment" /></td>
    </tr>
  </tbody>
</table>

第一個區塊中我叫 FunStuff.js 的 JavaScript 檔的邏輯是什麼被稱為一個準備好的函數,它將運行儘快呈現的文檔準備就緒。在我的功能,我的定義中所示的類型 viewModel 圖 4,我會使用綁定到我的 Web 表單中的評論意見表其注釋屬性。

圖 4 的 FunStuff.js 開始

var viewModel;
$(function() {
  viewModel = {
    comments: ko.observableArray(),
    addRange: addRange,
    add: add,
    remove: remove,
    exists: exists,
    errorMessage: ko.observable(""),
  };
  var serviceName = 'breeze/Comments';
  var vm = viewModel;
  var manager = new breeze.EntityManager(serviceName);
  getComments();
  ko.applyBindings(viewModel, 
    document.getElementById('comments'));
 // Other functions follow
});

準備好功能還指定了一些啟動代碼:

  • serviceName 定義的 Web API uri
  • vm 是為 viewModel 短的別名
  • 經理為 Web API 設置微風實體管理器
  • getComments 是一個方法,調用的 API,並返回資料
  • ko.applyBinding 是一種挖空方法要綁定到的意見表 viewModel

請注意我聲明瞭函數以外 viewModel。我需要訪問到它從一個腳本在.aspx 頁中以後,所以它不得不為外部可見度範圍。

在 viewModel 中的最重要的屬性是 observableArray 命名的評論。挖空將跟蹤的什麼是陣列中並更新綁定的表陣列發生變化時。其他屬性只是公開我已定義此啟動代碼通過 viewModel 下面的附加功能。

讓我們開始用 getComments 函數中所示圖 5

圖 5 查詢資料通過 Web API 使用的微風

function getComments () {
  var query = breeze.EntityQuery.from("Comments")
    .withParameters({ UserId: document.getElementById('hiddenId').value });
  return manager.executeQuery(query)
    .then(saveSucceeded).fail(failed);
}
function saveSucceeded (data) {
  var count = data.results.length;
  log("Retrieved Comments: " + count);
  if (!count) {
    log("No Comments");
    return;
  }
  vm.comments(data.results);
}
function failed(error) {
  vm.errorMessage(error);
}

在 getComments 函數中,我使用的微風執行我的 Web API 方法,評論,通過在當前使用者 Id 從 Web 頁上的隱藏欄位中。還記得在管理器變數已經上定義的 uri 的微風和評論。如果查詢成功,saveSucceeded 函數運行,記錄一些資訊在螢幕上的和推向 viewModel 的批註屬性的查詢的結果。在我的筆記本電腦,我可以看到空表之前非同步任務已完成,然後突然用填充表結果 (見圖 6)。而且記住,這發生在用戶端上。沒有回發發生所以它是流體的使用者體驗。


圖 6 評論從 Web API 檢索和 Knockout.js 的説明下與綁定**

쭮힗 4좬對框被選中和未選中的反應

下一個挑戰是使該回應使用者的選擇,從有趣的東西清單的清單。選中一個專案時,它需要添加或刪除從 viewModel.comments 陣列和綁定的表根據使用者是否是添加或刪除核取記號。更新該陣列的邏輯是在 JavaScript 檔中,但報警動作的模型的邏輯駐留在.aspx 頁面中的腳本中。它是可能要綁定功能,如核取方塊 onclick 到挖空,但我沒走這條路。

在.aspx 表單的標記中,添加下面的方法到頁面頁眉節:

$("#checkBoxes").click(function(event) {
  var id = $(event.target)[0].value;
  if (event.target.
          nodeName == "INPUT") {
    var name = $(event.target)[0].parentElement.textContent;
    // alert('check!' + 'id:' + id + ' text:' + name);
    viewModel.updateCommentsList(id, name);  }
});

這可能是由於我有一個 div,名為核取方塊周圍的所有動態生成核取方塊控制項的事實。 我使用 jQuery 抓取觸發事件和相關標籤中的名稱的核取方塊的值。 然後把那些給我 viewModel 的 updateCommentsList 方法。 警報只是為了測試了接線正確的函數。

現在讓我們看看 updateCommentsList 及相關的功能我的 JavaScript 檔中。 使用者可能會選中或取消選中一個專案,因此它需要添加或刪除。而不是擔心狀態的核取方塊,在我存在我只是讓幫我看看是否專案已經是陣列中的注釋的挖空 utils 函數的方法。 如果是,我需要將其刪除。 微風正在跟蹤更改,因為我從 observableArray 中刪除它,但告訴微風更改跟蹤,考慮將其刪除。 這有兩個作用。 第一,當保存時,微風將刪除命令發送到資料庫 (通過在我的案子 EF)。 但如果該專案再次檢查,並且需要重新添加到 observableArray,微風簡單地恢復它在更改跟蹤器。 否則,因為我使用複合鍵的標識的評論,有一個新的專案和具有相同的標識已刪除的郵件會造成衝突。請注意雖然挖空回應 push 方法添加專案,但我必須通知它陣列已經突變為它回應中刪除專案的順序。 再次,由於資料繫結,表動態更改為核取方塊都選中和未選中。

請注意當創建一個新的專案,我來拿從表單的標記中的隱藏欄位的使用者的使用者 Id。 在表單的 Page_Load 的原始版本中,我將此值設置後抓住使用者。 通過對每個專案在評論中栓的使用者 Id 和 FunStuffId,我可以存儲所有的意見,將它們關聯與正確的使用者和專案所需的資料。

Oncheck 有線向上和在回應中修改的評論 observableArray,我可以看到,例如,切換的手錶醫生誰核取方塊原因看醫生的人要的行顯示或消失根據核取方塊的狀態。

쭮힗 5좬保存的評論

我的頁面已經具有一個保存功能,保存的核取方塊被標記為 true,但現在我想要在同一時間使用另一個 Web API 方法保存批註。 現有的存儲方法執行在頁回以回應 SaveThatStuff 按鈕按一下時。 其邏輯是在代碼隱藏頁中。 我其實可以保存在伺服器端調用使用相同的按鈕按一下之前評論的用戶端調用。 我知道這是可能的與 Web 表單使用舊學校 onClientClick 的屬性,但在我修改時程表應用程式的情況下,我也不得不執行驗證,以確定是否任務小時數和時間工作表是準備好了要保存。 如果驗證失敗,做了已經忘了保存,Web API 不僅我不得不阻止以及執行回發和伺服器端保存方法。 硬的時間研究這出使用 onClientClick,鼓勵我再與 jQuery 實現現代化了 在我的回應在用戶端中的核取方塊按一下相同的方式,我可以有對 btnSave 所按一下的用戶端的回應。 而它會發生之前的回發和伺服器端的回應。 讓這兩個事件就一個按鈕的按一下操作,就像這樣:

$("#btnSave").click(function(event) {
  validationResult = viewModel.validate();
  if (validationResult == false) {
    alert("validation failed");
    event.preventDefault();
  } else {
    viewModel.save();
  }
});

我有一個存根 (stub) 驗證方法總是返回 true 的示例中,雖然我測試了可以肯定的事情正確行為如果它返回 false。 在這種情況下,我使用 JavaScript event.preventDefault 停止進一步的處理。 將不保存注釋,不但不回發和伺服器端保存將不會發生。 否則為我叫 viewModel.save,頁繼續與按鈕的伺服器端行為,保存使用者的 FunStuff 選擇。 我的 saveComments 函數是 viewModel.save,問微風實體管理器,以執行 saveChanges 所調用的:

function saveComments() {
  manager.saveChanges()
    .then(saveSucceeded)
    .fail(failed);
}

這反過來查找我的控制器 SaveChanges 方法並執行它:

[HttpPost]
  public SaveResult SaveChanges(JObject saveBundle)
  {
    return _contextProvider.SaveChanges(saveBundle);
  }

為此,我到 EF6 資料層添加評論,然後交換意見控制器的方法,對使用的微風伺服器端元件 (這會使對我 EF6 資料層的調用) 的資料庫執行查詢。 所以返回到用戶端的資料將資料從資料庫,SaveChanges 然後可以保存回資料庫。 你可以看到這在下載示例中,使用 EF6 和代碼第一次將創建和種子示例資料庫。

圖 7 JavaScript 為更新注釋清單中按一下核取方塊上的使用者回應

function updateCommentsList(selectedValue, selectedText) {
  if (exists(selectedValue)) {
    var comment = remove(selectedValue);
    comment.entityAspect.setDeleted();
  } else {
  var deleted = manager.getChanges().filter(function (e) {
    return e.FunStuffId() == selectedValue
  })[0];  // Note: .filter won't work in IE8 or earlier
  var newSelection;
  if (deleted) {
    newSelection = deleted;
    deleted.entityAspect.rejectChanges();
  } else {
    newSelection = manager.createEntity('FunStuffComment', {
      'UserId': document.getElementById('hiddenId').value,
      'FunStuffId': selectedValue,
      'FunStuffName': selectedText,
      'Comment': ""
    });
  }
  viewModel.comments.push(newSelection);    }
  function exists(stuffId) {
    var existingItem = ko.utils.arrayFirst(vm.comments(), function (item) {
      return stuffId == item.FunStuffId();
    });
    return existingItem != null;
  };
  function remove(stuffId) {
    var selected = ko.utils.arrayFirst
    (vm.comments(), function (item) {
    return stuffId == item.FunStuffId;
    });
    ko.utils.arrayRemoveItem(vm.comments(), selected);
    vm.comments.valueHasMutated();
  };

JavaScript 從我的朋友們一點説明

我在這個專案上和在建的這篇文章的示例工作,寫了比以前更多的 JavaScript。 它不是我的專業領域 (正如我已經指出頻繁地在此列),雖然我感到相當自豪的我完成了。 然而,知道很多讀者可能會看到一些這些技術中的第一次,我傾斜上Ward響鈴從 IdeaBlade (微風的創作者) 深入代碼審查,和一些程式設計來幫我清理我的微風的一些工作以及 JavaScript 和 jQuery 的對子。 除了或許為現在的"信"使用的 Knockout.js,您可以下載該示例提供一些好的經驗教訓。 但是,請記住,重點是提高一個舊的 Web 表單專案與這些更加現代的技術,並使最終使用者體驗愉快的那麼多。

Julie Lerman 是微軟最有價值球員,.NET 的導師和諮詢師,住在佛蒙特州的山裡。你可以找到她提出關於資料訪問和使用者組和會議,世界各地其他 Microsoft.NET 主題。在她博客 thedatafarm.com/blog 和的作者是"程式設計Entity Framework"(2010 年) 以及代碼第一版 (2011 年) 和 DbCoNtext 版 (2012 年),所有從 O'Reilly 媒體。跟著她在 Twitter 上 twitter.com/julielerman 看看她的 Pluralsight 課程 juliel.me/PS-視頻

感謝以下 Microsoft 技術專家對本文的審閱:達米安Edwards(dedward@microsoft.com) 和Scott的獵人 (Scott。Hunter@microsoft.com)