Quantcast
Channel: 黑暗執行緒
Viewing all 2311 articles
Browse latest View live

KO範例28 - 轉換對象(排除自己)之下拉選單連動

$
0
0

專案需求一枚,用於匯率換算。來源幣別及目標幣別以下拉選單方式顯示,想當然爾,目標幣別跟來源幣別相同還轉換個屁,因此規格提到: 目標幣別的下拉選項應包含所有幣別,但排除來源幣別當下的選取幣別,特殊的下拉選單連動需求應運而生。

要用KO實現此一連動並不困難: 為兩個<SELECT>各宣告一個observableArray作為options繫結對象,也各宣告一個observable作為value的繫結對象以對應下拉選單選取值;接著宣告一個computed函式建立來源幣別選取值與目標幣別observableArray的關聯,一旦來源幣別變動,複製來源幣別的observableArray,剔除當下來源幣別選取值後將陣列設定成目標幣別observableArray的內容,大功告成。

進一步分析,前述的computed只有單一訂閱對象及單一更新對象,可再簡化,將目標幣別options直接繫結到一個computed(訂閱來源幣別選取值,傳回剔除選取值後的幣別陣列),便可以computed取代observableArray + computed。

最後提一下jQuery.map這個好東西,在JavaScript中處理陣列,許多初學者直覺的做法會宣告新的空陣列,跑迴圈逐一處理陣列元素再放入新陣列。例如: 需求為1-10的數字陣列只留單數並+10,變成11, 13, 15..,通常會寫成:

var orig = [1,2,3,4,5,6,7,8,9,10]; 
  var res = []; 
  for (var i = 0; i < orig.length; i++) { 
    var v = orig[i]; 
    if (v % 2 == 0) continue; 
    res.push(v + 10); 
  }

而jQuery.map可簡化上述程式,變成:

var orig = [1,2,3,4,5,6,7,8,9,10]; 
var res = $.map(orig, function(v, i) { 
  if (v % 2 == 0) return null; //表示剔除 
  return v + 10; 
});

很精省吧?

完整程式碼如下: 線上展示

<!DOCTYPEhtml>
<html>
<head>
<metacharset=utf-8/>
<title>KO範例28 - 轉換對象(排除自己)之下拉選單連動</title>
</head>
<body>
<div>
    From: 
<selectdata-bind="options: ListA, value: PropA, optionsText: 't', optionsValue: 'v'"></select>
    (<spandata-bind="text: PropA"></span>)
&nbsp;&nbsp;&nbsp;To:
<selectdata-bind="options: ListB, value: PropB, optionsText: 't', optionsValue: 'v'"></select>
    (<spandata-bind="text: PropB"></span>)
</div>
<scriptsrc="http://ajax.aspnetcdn.com/ajax/jQuery/jquery-1.9.1.js"></script>
<script src="http://ajax.aspnetcdn.com/ajax/knockout/knockout-2.2.1.js"></script>
<script>
function myViewModel() {
var self = this;
      self.PropA = ko.observable();
      self.ListA = ko.observableArray([
        { v:"TWD", t:"台幣" }, 
        { v:"USD", t:"美金" },
        { v:"EUR", t:"歐元" },
        { v:"JPY", t:"日幣" }, 
        { v:"HKD", t:"港幣" }
      ]);
      self.PropB = ko.observable();
      self.ListB = ko.computed(function() {
var a = self.PropA();
return $.map(self.ListA(), function(item) {
if (item.v == a) returnnull;
return item;
        });
      });
    }
var vm = new myViewModel();
    ko.applyBindings(vm);
</script>
</body>
</html>

[KO系列]

http://www.darkthread.net/kolab/labs/default.aspx?m=post

【茶包射手日記】VS2012 Update 3安裝失敗

$
0
0

在一台新裝的Windows 7安裝VS2012,啟動後出現VS2012 Update 3更新通知,順勢執行Update 3安裝程式,遇到以下錯誤:

Setup Failed! Install cannot continue because some required components failed.
Setup Engine: The pipe is being closed.

錯誤訊息對話框還算貼心,可直接導引到Log檔,看到一堆錯誤如下:

Error 0x800700e8: Failed to write message type to pipe.
Error 0x800700e8: Failed to write send message to pipe.

由關鍵字0x800700e8很快找到MSDN討論區的文章,遇到類似問題的朋友不少,判斷是Web Installe的問題,微軟的論壇客服建議直接下載ISO檔安裝。下載ISO檔(大小約2.08GB)執行VS2012.3.exe即可順利安裝,搞定收工!

Json.NET技巧兩則: 忽略屬性及列舉轉字串

$
0
0

開發主戰場漸漸移到前端,應用Json.NET的深度也逐步增加。今天學會Json.NET技巧兩則,筆記如下:

情境1:

待序列化物件的部分屬性純粹供.NET端應用,不需傳到前端,希望能予以排除以縮短JSON字串,減少頻寬浪費。

解決方案

方法1 - 在屬性加上[JsonIgnore],負向表列哪些屬性不要序列化。
方法2 - 在類別加上[DataContract],為需序列化屬性加上[DataMember],正向表列哪些屬性需要序列化。
參考: Efficient JSON with Json.NET – Reducing Serialized JSON Size

情境2

預設列舉(Enum)屬性會被序列化為對應數值(int),但在許多應用場合,轉為列舉項目文字更有可讀性。

解決方案

為列舉屬性加上[JsonConverter(typeof(StringEnumConverter))]就搞定囉!

來個範例程式展示效果:

using Newtonsoft.Json;
using Newtonsoft.Json.Converters;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
 
namespace ConsoleApplication1
{
class Program
    {
publicenum Options
        {
            Option1, Option2
        }
 
publicclass Entity
        {
public Options A1 { get; set; }
public Options A2 { get; set; }
            [JsonConverter(typeof(StringEnumConverter))]
public Options B1 { get; set; }
            [JsonConverter(typeof(StringEnumConverter))]
public Options B2 { get; set; }
publicstring P1 { get; set; }
            [JsonIgnore]
publicstring P2 { get; set; }
        }
 
 
staticvoid Main(string[] args)
        {
            Entity ent = new Entity()
            {
                A1 = Options.Option1,
                A2 = Options.Option2,
                B1 = Options.Option1,
                B2 = Options.Option2,
                P1 = "P1",
                P2 = "P2"
            };
            Console.WriteLine(
                JsonConvert.SerializeObject(ent, Formatting.Indented));
            Console.Read();
        }
    }
}

執行結果如下,其中A1, A2兩個列舉屬性被轉成0與1(Json.NET的預設行為),B1, B2列舉屬性因加上[JsonConverter(typeof(StringEnumConverter))]宣告,被轉為"Option1"及"Options2";加上[JsonIgnore]的P2屬性,則被排除在JSON字串之外:

{
  "A1": 0,
  "A2": 1,
  "B1": "Option1",
  "B2": "Option2",
  "P1": "P1"
}

原本擔心要費點手腳的兩個需求,透過Attribute宣告就輕易搞定,不禁讚嘆Json.NET考慮周詳,很好很強大,實在該直接收入.NET Framework才對!!

【答客問】Json.NET-動態決定屬性是否序列化

$
0
0

昨天提到Json.NET屬性序列化設定,接獲讀者森哥留言:

請問黑大,
針對不需要序列化的「屬性」是否可以透過程式「動態」設定或是過濾?

有預感遲早也會遇到這個靠杯火盃的考驗,決定打鐵趁熱,馬上來練習。所幸,Json.NET真的很強大,早就料想到此一需求,提供ContractResolver以實現神乎奇技的高度動態化。

我寫了一個範例,展示兩種動態決定應序列化屬性的情境:

  • Serialize時傳入屬性名稱陣列作為參數,正向表列JSON應包含的屬性。
  • 由物件屬性值決定屬性是否要序列化,例如: 如果是女生就不包含年齡。(這幾乎已彈性到極點,雖然實務上不常用到)

程式的做法是宣告兩個繼承自DefaultContractResolver的類別: LimitPropsContractResolver在建構時傳入string[]參數列出要序列化的屬性名稱,並覆寫CreateProperties方法,過濾base.CreateProperties()傳回的IList<JsonProperty>,只保留前述string[]有列出的屬性;HideAgeContractResolver則覆寫CreateProperty()方法,由base.CreateProperty()取得JsonProperty,JsonProperty有個ShouldSerialize屬性可以傳入Lambda運算式,逐筆處理每個要序列化的物件,在Lambda運算式中可將物件轉型為原型別進行判斷,若不要序列化就傳回false。

using System;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using System.Text;
using Newtonsoft.Json;
using Newtonsoft.Json.Converters;
using Newtonsoft.Json.Serialization;
 
namespace ConsoleApplication1
{
class Program
    {
publicenum Gender 
        {
            Male, Female
        }
 
publicclass Person
        {
publicstring Name { get; set; }
            [JsonConverter(typeof(StringEnumConverter))]
public Gender Gender { get; set; }
publicint Age { get; set; }
public Person(string name, Gender gender, int age)
            {
                Name = name; Gender = gender; Age = age;
            }
        }
 
publicclass HideAgeContractResolver : DefaultContractResolver
        {
//REF: http://james.newtonking.com/projects/json/help/index.html?topic=html/ContractResolver.htm
protectedoverride JsonProperty CreateProperty(MemberInfo member, 
                MemberSerialization memberSerialization)
            {
                JsonProperty p = base.CreateProperty(member, memberSerialization);
if (p.PropertyName == "Age")
                {
//依性別決定是否要序列化
                    p.ShouldSerialize = instance =>
                    {
                        Person person = (Person)instance;
return person.Gender == Gender.Male;
                    };
                }
return p;
            }
        }
 
publicclass LimitPropsContractResolver : DefaultContractResolver
        {
string[] props = null;
public LimitPropsContractResolver(string[] props)
            {
//指定要序列化屬性的清單
this.props = props;
            }
//REF: http://james.newtonking.com/archive/2009/10/23/efficient-json-with-json-net-reducing-serialized-json-size.aspx
protectedoverride IList<JsonProperty> CreateProperties(Type type, 
                MemberSerialization memberSerialization)
            {
                IList<JsonProperty> list = 
base.CreateProperties(type, memberSerialization);
//只保留清單有列出的屬性
return list.Where(p => props.Contains(p.PropertyName)).ToList();
            }
 
        }
 
staticvoid Main(string[] args)
        {
            List<Person> list = new List<Person>();
            list.Add(new Person("George", Gender.Male, 18));
            list.Add(new Person("Mary", Gender.Female, 40));
//正常輸出
            Console.WriteLine(JsonConvert.SerializeObject(
                list, Formatting.Indented));
            var settings = new JsonSerializerSettings();
//加上ContractResolver,正向表列哪些屬性要序列化
            settings.ContractResolver = 
new LimitPropsContractResolver("Name,Age".Split(','));
            Console.WriteLine(JsonConvert.SerializeObject(
                list, Formatting.Indented, settings));
//加上ContractResolver,依物件的屬性值動態決定要不要序列化
            settings.ContractResolver = new HideAgeContractResolver();
            Console.WriteLine(JsonConvert.SerializeObject(
                list, Formatting.Indented, settings));
            Console.ReadLine();
 
        }
    }
}

程式執行結果如下,共有三段輸出,第一段為正常版;第二段套用LimitPropsContractResolver("Name,Age".Split(',')),故JSON中只見Name及Age,Gender被隱藏;第三段套用了HideAgeContractResolver(),如結果所示,Mary的JSON內容不包含年齡,George則包含。

[
  {
    "Name": "George",
    "Gender": "Male",
    "Age": 18
  },
  {
    "Name": "Mary",
    "Gender": "Female",
    "Age": 40
  }
]
[
  {
    "Name": "George",
    "Age": 18
  },
  {
    "Name": "Mary",
    "Age": 40
  }
]
[
  {
    "Name": "George",
    "Gender": "Male",
    "Age": 18
  },
  {
    "Name": "Mary",
    "Gender": "Female"
  }
]

演練完畢,內心激動澎湃,對Json.NET的景仰如淊淊江水,綿綿不絕~

如果奧斯卡有最佳元件獎,我提名它!

【笨問題】IE9的「console未被定義」錯誤

$
0
0

一直以來,有個鬼現象纏著我揮之不去,console.log常因不明原因在IE9出現SCRIPT5009: 'console' is undefined (console未被定義) 錯誤!

我當然知道IE從IE8+才支援console物件,但如上圖所示,網頁明明是IE9標準模式,為什麼IE9卻說console物件不存在? 但進行偵錯,console.log()卻又正常!

笨了好久,今天才解開謎團:

 IE8/IE9要先按F12開啟IE Dev Tools才能存取console物件啦! 笨蛋!

參考來源: http://msdn.microsoft.com/en-us/library/ie/gg589530(v=vs.85).aspx

… You use the console object to send a message to the console from your code. Using the console instead of "window.alert()" when testing code is less obtrusive and doesn't stop you with a modal dialog box. This object provides a number of forms so that you can distinguish between informational messages and error messages if you want. When you use the console object, make sure that the F12 tools are open. To avoid executing needless code, use the following feature test …

所以,如果使用環境包含IE8/9,請養成良好習慣,用if (window.console) { ... }包住console.log()動作,切忌把IE8/9想成Chrome/Firefox,以為永遠有window.console可用!

PS: 終於,IE10改邪歸正向Chrome/Firefox看齊,console不再像段譽的六脈神劍時有時無。但只要IE8/9還在一天,console檢查還是不能少。

讓IE直接顯示JSON結果

$
0
0

IE有個討人厭的行為。

當網頁程式以JSON格式傳回結果(JSON字串,且Content-Type設為application/json),在IE需下載另存檔案才能檢視,不像Chrome或Firefox可直接看結果。當需要反覆測試,"重新整理網頁->選取位置另存檔案->開檔看結果"的迴圈容易誘發焦慮、煩躁、爆怒等症狀,而一般患者多會啟動生物本能,默默關上IE改開Chrome尋求解脫...

例如,以下MVC Controller的GetJson()會以JSON格式傳回字串:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Web;
using System.Web.Mvc;
 
namespace MyWeb.Controllers
{
publicclass HomeController : Controller
    {
public ActionResult Index()
        {
return View();
        }
 
public ActionResult GetJson()
        {
return Json("This is a test", JsonRequestBehavior.AllowGet);
        }
    }
}

使用IE測試,會彈出下載存檔提示:

手邊的專案需用IE測試,又不想為了測API切換不同瀏覽器,爬文求解,在stackoverflow找到很棒的解法。其原理是修改Registry,將application/json、text/json兩種Content-Type開啟設定調成與GIF/PNG/HTML一致,改為直接用瀏覽器檢視。

將以下內容存檔為ie-json-fix.reg,在Windows中點擊執行安裝即可完成Registry修改。

Windows Registry Editor Version 5.00
;
; Tell IE to open JSON documents in the browser.  
; 25336920-03F9-11cf-8FD0-00AA00686F13 is the CLSID for the "Browse in place" .
;  
 
[HKEY_CLASSES_ROOT\MIME\Database\Content Type\application/json]
"CLSID"="{25336920-03F9-11cf-8FD0-00AA00686F13}"
"Encoding"=hex:08,00,00,00
 
[HKEY_CLASSES_ROOT\MIME\Database\Content Type\text/json]
"CLSID"="{25336920-03F9-11cf-8FD0-00AA00686F13}"
"Encoding"=hex:08,00,00,00

修改後,IE就能會Chrome一樣直接顯示JSON內容囉! It Rocks!

VS2012 JavaScript IntelliSense會忽略底線起首的屬性

$
0
0

我寫了一個CRUD輔助程式庫,內建自動抓取ViewModel進行JSON序列化的功能,其中有條"序列化時排除ViewModel中名稱為底線開頭屬性"的慣例(Convension)。前陣子接到同事回報,配合此慣例宣告ViewModel,卻發現底線起首的屬性會被Visual Studio 2012的JavaScript IntelliSense無視。

寫一小段程式,馬上獲得驗證:

一直沒留意VS2012的JS IntelliSense有此特性,查詢MSDN文件後獲得證實(在Handling IntelliSense events小節)。原來這是JS Intellisense強大擴充能力的一環,過濾底線開頭屬性的程式被放在VS2012的安裝目錄JavaScript\References\underscorefilter.js。

既然過濾行為由underscorefilter.js控制,要修改並非難事。不過,屬性名稱以底線開頭在JavaScript慣例隱含"私有"、"限內部使用"的意義,不應開放外界呼叫,因此不列在自動完成清單十分合理! 不想打破這個立意良好的設計,最後決定調整程式庫慣例,改成"序列化時排除名稱為$開頭屬性"!

案外案: 約定以$為屬性字首慣例,在C#端再踢到小石頭一顆: .NET識別名稱命名規則只允許字母或底線當字首,無法在C#中使用$作為屬性名稱字首與JavaScript端對應。最後的解決做法是在C#命名為"priv_",JSON序列化送到JavaScript端後再將"priv_Boo"改成"$Boo"。

IE9 Bug-滑鼠移過導致高度增加

$
0
0

日前曾遇此蟲,爬文解決而未寫KB。今日同事再遇,腦中只餘殘存印象,戳力苦思蒐羅,方復拾回,嗔甚,為文誌之以杜後患。

網友Brian Richards對這個問題做了清楚的分析,也提交到MS Connect,其發生條件包含:

  • DIV設為overflow: auto且指定寬度
  • DIV內含TABLE,寬於DIV而產生水平捲軸
  • TD中元素使用CSS :hover進行樣式變化

以下範例可重現問題:

<!DOCTYPEhtml>
<html>
<head>
<metacharset=utf-8/>
<title>IE9 Bug</title>
<style>
    .lnk { cursor: pointer; color: white; }
    .lnk:hover { color: yellow; }
</style>
</head>
<body>
<divclass="container"
style="overflow: auto; width: 200px; background-color: gray;">
<tablestyle="width: 300px">
<tr><td><spanclass="lnk">Line1</span></td></tr>
<tr><td><spanclass="lnk">Line2</span></td></tr>
<tr><td><spanclass="lnk">Line3</span></td></tr>
</table>
</div>
<div>Bottom of Page</div>
</body>
</html>

示範如下: 線上展示(需用IE9檢視)

解決方法: DIV加上CSS min-height: 0%即可避免。線上展示

PS: 此問題在IE10已修正,但IE9應該是不打算修了。IE Team心理應該也悶,Chrome、Firefox Team表示: 舊版是什麼? 能吃嗎?


【茶包射手日記】ASP.NET MVC 4專案4.5 Framework轉4.0

$
0
0

記錄在Windows Server 2003 IIS6部署ASP.NET MVC 4專案的經驗。

部署第一步是複製檔案、設定Web Application、萬用字元應用程式對應等。(相關眉角可參考保哥的ASP.NET 4.0 安裝在 IIS6 最常遇到的四個問題)

第一枚茶包現身! 當初新增專案時一時不察,選了.NET Framework 4.5,web.config有兩行設定:

<compilation debug="true" targetFramework="4.5" />
<httpRuntime targetFramework="4.5" />

移到Windows 2003立即引爆。由於Windows 2003不支援.NET 4.5,只好乖乖地開Visual Studio將MVC專案降版為.NET 4.0。web.config內容被Visual Studio改成:

<compilation debug="true" targetFramework="4.0"/>
<httpRuntime/>

重新部署檔案,正期待看到網頁,另一枚茶包冒出來澆我一頭冷水:

[FileNotFoundException: Could not load file or assembly 'System.Net.Http, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a' or one of its dependencies. The system cannot find the file specified.]
   System.Web.Http.GlobalConfiguration..cctor() +0

[TypeInitializationException: The type initializer for 'System.Web.Http.GlobalConfiguration' threw an exception.]
   System.Web.Http.GlobalConfiguration.get_Configuration() +14
   MyWeb.Areas.HelpPage.HelpPageAreaRegistration.RegisterArea(AreaRegistrationContext context) +94
   System.Web.Mvc.AreaRegistration.CreateContextAndRegister(RouteCollection routes, Object state) +104
   System.Web.Mvc.AreaRegistration.RegisterAllAreas(RouteCollection routes, IBuildManager buildManager, Object state) +195
   System.Web.Mvc.AreaRegistration.RegisterAllAreas(Object state) +27
   MyWeb.WebApiApplication.Application_Start() +12

推測是Visual Studio的4.5到4.0轉換不夠完整所致,決定另開一個.NET 4.0 MVC4專案比對最直接有效。很快找到一項明顯差異:

.NET 4.5轉.NET 4.0的MVC4專案,System.Net.Http.dll的參照來源是:
C:\Program Files (x86)\Microsoft ASP.NET\ASP.NET MVC 4\Assemblies\System.Net.Http.dll

而原生.NET 4.0 MVC4專案,System.Net.Http.dll則來自NuGet:
..\packages\Microsoft.Net.Http.2.0.20710.0\lib\net40\System.Net.Http.dll

由於System.Net.Http在.NET 4.5才納入BCL,猜想就是造成在.NET 4.0無法運作的元凶。問題專案先移除System.Net.Http參照,重新取用NuGet提供的版本,重新編譯、發佈後bin目錄多了System.Net.Http.dll與System.Net.Http.WebRequest.dll兩個檔案,再試一次,問題排除!!

ASP.NET MVC Filter練習-限定本機存取

$
0
0

在ASP.NET MVC專案新增了開發偵錯專用的Controller,某些Action想限定從localhost存取,以免遭到誤用。逐一在Action加入檢查IP邏輯是種做法,但如此有點浪費ASP.NET MVC強大的擴充性,就好比提著子彈上膛的M16步槍上戰場,不扣板機卻拿槍托狂敲敵人的頭,不免有暴殄天物之憾。

ASP.NET MVC有個Filter機制(中文翻成篩選器),在Filter可自訂執行Action時要一併觸發的邏輯,為Action加上[FilterName] Attribute,便可在該Action之前或之後插入自訂邏輯。這種概念很適合用來實現Log記錄、權限控管、Exception處理... 等等通用性任務。而IAuthorizationFilter介面專司權限管控,符合IP篩選的安全性質,因此我的構想是寫個類別,實做IAuthorizationFilter介面的OnAuthorization()方法,由傳入的AuthorizationContext取得UserHostAddress判斷來源IP,若HttpRequest不是來自本機(localhost)即拋出錯誤,就實現了Action只開放本機存取的效果。

FilterAttribute完成後,任何Action只要加上該FilterAttribute宣告,就會自動套用上述檢查限定本機存取,非常簡便易用。

以下是完整程式範例,為了增加應用彈性,我特別再抽出一層AllowedIpOnlyAttribute,宣告時傳入允許存取的IP清單;而LocalhostOnlyAttribute繼承AllowedIpOnlyAttribute,將允許IP寫死::1(IPv6)及127.0.0.1,成為AllowedIpOnlyAttribute的特例,做到限定本機存取。如此一魚兩吃,一次獲得兩種Filter,適用於不同情境。

using System;
using System.Collections.Generic;
using System.Linq;
using System.Web;
using System.Web.Mvc;
 
namespace MyWeb.Models
{
publicclass AllowedIpOnlyAttribute : FilterAttribute, IAuthorizationFilter
    {
privatestring[] ipList = newstring[] {};
//建構式接收以逗號或分號分隔的IP清單,限定存取來源
//TODO: 如要方便事後修改,可擴充成由config讀取IP清單,但會增加被破解風險
public AllowedIpOnlyAttribute(string allowedIps)
        {
            ipList = allowedIps.Split(',', ';');
        }
#region IAuthorizationFilter Members
publicvoid OnAuthorization(AuthorizationContext filterContext)
        {
//實作OnAuthorization,當來源IP不在清單上,彈出錯誤
string clientIp = filterContext.HttpContext.Request.UserHostAddress;
if (!ipList.Contains(clientIp))
thrownew ApplicationException("Disallowed Client IP!");
        }
#endregion
    }
//限定本機存取為AllowedIpOnlyAttribute的特殊情境,限定IP=::1或127.0.0.1
publicclass LocalhostOnlyAttribute : AllowedIpOnlyAttribute
    {
public LocalhostOnlyAttribute()
            : base("::1;127.0.0.1")
        {
        }
    }
}

接著,見識Filter便利性的時刻來了,在Action加上AllowedIpOnly或LocalhostOnly,Action立刻變成限定特定IP或本機IP才能使用! 很方便吧?

        [AllowedIpOnly("192.168.1.100")]
public ActionResult IpOnly()
        {
return Content("IpOnly");
        }
        [LocalhostOnly]
public ActionResult LocalhostOnly()
        {
return Content("LocalHostOnly");
        }

ASP.NET MVC路由練習-API分版

$
0
0

手邊ASP.NET MVC專案有個隱藏需求,預計上線不久要推出新版,有一段時間新舊版本並存。有幾個供AJAX呼叫的API性質Controller,希望未來出新版時名稱能沿用,不要弄出BooV1Controller、BooV2Controller這種名字,最好在URL路徑加上v1/v2等就能搞定,例如:

第一版: ~/api/v1/boo/action
第二版: ~/api/v2/boo/action

在ASP.NET MVC專案處理此類路徑問題,我想到兩個選擇: 一個是利用Area機制、另一個則是直接用Route對應。考量Area切割範圍涵蓋Controller、Model及View,我只想區分Controller版本,搬出Area有點殺雞用牛刀,就用路由解決吧!

構想如下,ASP.NET MVC專案在Controllers下新增V1及V2兩個資料夾,其中各放入一個Controller類別,由於Namespace不同(Mvc4Lab.Controllers.V1 vs Mvc4Lab.Controllers.V2),兩個Controller可使用相同命名,都叫BooController。

我們都知道ASP.NET MVC是"以習慣取代配置"(Convention over Configuration),會試圖從URL中找出Controller名稱,主動尋找專案中同名的"***Controller類別"處理該URL請求。當同一專案中出現相同名稱Controller,得額外提供路由線索,ASP.NET MVC才能正確解析。

原以為得費點手腳,沒想到MapRoute()設想很周到,只需在設定路徑時額外傳入Namespace參數(支援字串陣列,限定只在指定命名空間尋找Controller),便能做到不同URL格式使用同名Controller的效果。

在App_Strat/RouteConfig.cs中加兩ApiV1, ApiV2兩則額外路徑,大功告成,就這麼簡單。

using System;
using System.Collections.Generic;
using System.Linq;
using System.Web;
using System.Web.Mvc;
using System.Web.Routing;
 
namespace Mvc4Lab
{
publicclass RouteConfig
    {
publicstaticvoid RegisterRoutes(RouteCollection routes)
        {
            routes.IgnoreRoute("{resource}.axd/{*pathInfo}");
            routes.MapRoute(
                name: "ApiV1",
                url: "api/v1/{controller}/{action}",
                namespaces: newstring[] { "Mvc4Lab.Controllers.V1" }
            );
            routes.MapRoute(
                name: "ApiV2",
                url: "api/v2/{controller}/{action}",
                namespaces: newstring[] { "Mvc4Lab.Controllers.V2" }
            );
            routes.MapRoute(
                name: "Default",
                url: "{controller}/{action}/{id}",
                defaults: new { controller = "Home", action = "Index", 
                    id = UrlParameter.Optional }
            );
        }
    }
}

不過要提醒,由於ASP.NET MVC下所有的Request都要先進行路由解析再決定處理方式,切忌把路徑設定規則搞得太多太複雜,以免拖累系統效能。

【笨問題】紅杏出牆的internal類別

$
0
0

以下的.NET專案,有什麼不合理之處?

.NET開發者都知道,.NET有四種存取層級: public、private、protected及internal。public是公開對外、private只限同類別內存取、protected對繼承的子類別開放、而internal則只對同一Assembly內的其他的型別公開。在接手維護的專案看到如上述圖片的奇蹟: 有顆DLL宣告為internal的類別,卻大大方方地在另一顆DLL登場,如同public類別一樣被宣告使用,跨越了Assembly的藩籬,顛覆我對internal的認知,目瞪口呆之餘脫口而出 -- 花惹發!

以為是什麼密技魔法,追了好一陣子才想起,啊! Friend Assembly~

這是.NET 2.0起就支援的特性,為Assembly加上[assembly: InternalsVisibleTo("AnotherAssembly")],AnotherAssemly就會被當成自己人,允許存取標為internal的型別及成員。這個技巧常被應用在單元測試,解決測試程式無法存取internal成員的困擾。依據MSDN的說法,Friend Assembly主要有兩種用途:

  • 在單元測試期間,測試程式碼在另一個組件中執行,但是需要存取所測試組件中的成員,此時可以將該成員標記為 Friend (Visual Basic) 或 internal (C#)。
  • 您在開發類別庫時,類別庫的附加功能包含在不同組件中,且需要存取現有組件中的成員,此時可以將該成員標記為 Friend (Visual Basic) 或 internal (C#)。

這回遇到的顯然是後者。寫.NET程式十年有餘,卻沒見過野生的Friend Assembly,初見大驚小怪,有失老鳥的端莊儀態,見笑了。也再次證明  -- 學海無涯啊~~~

【茶包射手日記】TEMP指向RAMDisk導致Windows更新失敗

$
0
0

前幾天,工作機出現Windows更新重開提醒,從善如流,重開後卻看到駭人畫面:

Failure configuring Windows updates
Reverting chanes.
Do not turn off your computer

系統還原修改後自動重開,卻還是出現類似訊息(有Preparing to retry等訊),之後再度自動重開。心頭一涼,該不會Windows 2008R2就此癱瘓吧? 所幸,歷經兩次自動重開後恢復正常登入,看似一切正常,不禁讚美Windows自我修復能力不錯,有種死裡逃生的僥倖感。

今天適逢Visual Studio 2013 RTM,吃飯的傢伙當然也要灌頂一番。安裝VS2013得先升級到IE10,這才發現上回更新失敗自動還原後問題並沒有根本解決,Configure失敗的Windows Update仍在,卡住了其他的Windows更新,未更新完全就無法安裝VS2013。

由Windows Update的View update history展開追查。

由歷史記錄可看到有問題的更新是KB2864202。

循著編號KB2864202及Failure configuring Windows updates Reverting changes關鍵字爬文,很幸運地查到一篇網友經驗分享,提到TEMP暫存資料夾放在RAMDisk會導致類似問題,剛好與我的情境相同。

將TMP及TEMP路徑調回一般硬碟重新再試,Update果真順利安裝完成,感謝萬能的Google大神跟網友無私的分享。

更新問題順利解決,Visual Studio 2013裝好,卻已是十幾個小時之後,但這又是另一段茶包射手故事...

【茶包射手日記】惱人的IE10 9C59安裝錯誤

$
0
0

地表上最強大的開發工具 -- Visual Studio 2013於10/17 RTM了!!

公司的工作機是吃飯的傢伙,豈有不馬上升級的道理? 先前在家裡Windows 8安裝順暢無比,沒想到要裝在Windows 2008 R2卻歷經千辛萬苦,如赴西天取經一般波折...

我的Windows 2008 R2要安裝Visual Studio 2013,有兩項事前準備工作: 完成最新的Windows Update更新並升級IE10。因RAMDisk TEMP導致Windows Update更新失敗的鬼故事昨天已經說過了,而為了配合使用者環境,工作機的IE版本仍停留在IE9,這回不升級不行(反正IE11已隨Win8.1與VS2013同一天推出,公司IE版次推升也是早晚的事),Just Do It!

原本以為下一步下一步就可以搞定的IE10安裝,卻意外磨人,帶來一連串令人挫折的安裝經驗。

由網站下載IE10安裝時出現"Internet Explorer did not finish installing"錯誤訊息,試著透過Windows Update安裝一樣失敗收場,但多了個Code 9C59錯誤訊息:

由Windows\IE10_main.log可以看到詳細錯誤訊息: (經比對,不管使用安裝程式或是透過Windows Update安裝,背後發生的錯誤是相同的)

00:11.871: INFO:    Installing with the downloaded package. C:\Windows\TEMP\IE1D91F.tmp\IE10-neutral.Downloaded.cab
00:11.871: INFO:    Launched package installation: C:\Windows\SysNative\dism.exe /online /add-package /packagepath:C:\Windows\TEMP\IE1D91F.tmp\IE10-neutral.Downloaded.cab /quiet /norestart
00:18.080: INFO:    Process exit code 0x80092004 (-2146885628) [Cannot find object or property. ]
00:18.080: ERROR:   Neutral package installation failed (exit code = 0x80092004 (2148081668)).

有明確的錯誤訊息,理論上很快能在網路找到答案,沒想到相關討論滿坑滿谷,遇到問題的人很多,各式建議五花八門,卻沒有明確結論或解法。依我推測,這個看似清楚的錯誤訊息其實並不明確(說不定像"發生一般性的錯誤"一樣含糊),背後成因很多,大家實際遇到的魔王、小妖不盡相同但訊息卻相似,才會出現某人說有神效的療法,其他人試了連汗毛都沒搖一下。微軟甚至出了一份IE10安裝茶包指南,靠! 是有沒有這麼難裝?

總之,效法神農嚐百草,我試了以下做法:

  1. 解除特定Windows Update: KB2422556
  2. 升級顯示卡驅動程式
  3. 使用Windows更新整備工具修補更新
  4. 用pkgmgr清除更新整備工具回報損壞的Microsoft-Windows-IE-Hyphenation-Parent-Package及Microsoft-Windows-IE-Spelling-Parent-Package。[有人建議連先前版本(IE8, IE9)的套件也一併移除,擔心胡亂清除會有後遺症,我只移了確定破損的項目]
  5. 使用SFC /SCANNOW修復損壞的系統檔
  6. 學會看CBS.log、dism.log、CheckSUR.log...
  7. 進入Clean Boot模式再試
  8. 移除IE9重新安裝一次IE9
  9. 有人說要重裝OS... (花惹發! 雖被茶包磨到歸藍波火,但拎杯神智還清楚,沒照辦)
  10. ...

上方法全都試完,問題依舊... 不,是更糟了!! 移除了IE9打算重裝,但卻發現IE9重裝也失敗。這下可好,不但沒升到IE10,反而退回IE8,HTML5專案要怎麼測試? 長嘆一口氣,絕望到想輕生...

天無絕人之路,排除IE9無法安裝問題時,在微軟技術支援網站學到一招:

FORFILES /P %WINDIR%\servicing\Packages /M Microsoft-Windows-InternetExplorer-*9.*.mum /c "cmd /c echo Uninstalling package @fname && start /w pkgmgr /up:@fname /norestart"

先前網路上看過pkgmgr移掉IE套件的解法,但我只移除了偵測到損壞的項目,而由上述做法推敲,將該版本所有套件都清除才能解決問題(而且是安全的),有官方文件背書決定放手一搏,將上述Script中的*9.*.mum改成*10.*.mum,執行清除後再重新安裝。喵的,IE10安裝成功,我感動到都要哭了~

終於,Visual Studio 2013在工作機上安裝就緒,Let's Party!

2013艋舺馬拉松~

$
0
0

第11馬,2013艋舺馬拉松。

賽前與教練團討論,總教頭謎之聲強力主張: 因後面尚有貓空及田中馬馬相連,加以腳傷未瘉及本屆"辦桌"重現江湖,故為本場賽事定調,提出16字箴言: 戒急用忍,保全實力;從容不迫,主攻辦桌。聽到總教練這麼說,心中無比寬慰... (盛竹如旁白: 開心接下總教練指示,這時的黑大並不知道,自己正一步步墮入廢柴深淵~~~)

五點多來到華中橋,天色仍昏暗,辦桌用的200組桌椅已擺放就緒。起跑前夕烏雲籠罩,下起一陣雨(右上照片還剛好拍到一根雨絲哩),有點擔心會重演去年冒雨完賽的考驗,所幸鳴槍前一刻雨勢轉小,事實上,老天爺今天還賞了個晴時多雲的好天氣。

今年的賽道要來回折返三趟,河濱道真的窄了點,排在5:00-5:30實力區,起跑後兩公里人潮濃得化不開,速度都在七分速以下。不過對我這種後半段打算當步兵的涼涼組來說,這種小小延遲根本不需要放在心上。隨著接近中午,天氣愈來愈好,一片藍天白雲,但雲層削弱不少日曬,造就出不錯的跑步天氣。

另外,我要大推華中橋下的幾支大音響喇吧,大聲放著電音舞曲,每回經過都忍不住衝上一小段,這才發現原來"凍次凍次"跟跑步這麼搭, 哈!

補給站精彩度一如往昔,運動飲料與水是基本配備,後來連沙士、雪碧也出籠,還夾雜一些蠻牛。除了香蕉、葡萄、香瓜、鹹蛋外,今年還出現豆干與香腸(沒給蒜頭,好可惜),連必魯(Beer啤酒)都上陣,根本是下酒菜組合啊! 幸好這些沒出現在同一攤,否則難保不會有跑友在路邊"罷掀啊罷掀、棄翹啊棄翹"划起酒拳。

去年的雨中艋舺馬,飛小魚的"跑步吧! 人生"網路廣播伴我全程。今天在最後十來公里,忽然聽到對向跑友叫了聲"小魚 加油",轉頭一看,飛小魚就在我後方(號碼布寫的就是"飛小魚"耶! 酷~),去年只聞其聲,今年見到本人(下圖右上紅衣背影),分外有趣。最後,以5:03:38秒完賽,還完晶片20分鐘就領到成績證明,快速如昔,終點站的冰仙草超好喝,忍不住連乾數碗。

重頭戲來了,辦桌登場。大會事先調查過跑友的預估完賽時間,將差不多時間的跑友安排在一桌,原本以為大家素不相識,湊在一起吃飯會冷場尷尬,但顯然多慮了。每個人都有滿滿的跑步經,話匣一開聊也聊不完! 哈,如果有機會跟不認識的Developer併桌吃飯,我們應該可以從機車客戶、水母PM聊到機房乖乖奇譚,應該也會一樣熱絡吧?

今年的獎牌為銀色亮面款,十分好看,私人收藏再添一枚漂亮獎牌。

【補記】

跑前幾天GPS錶的USB孔防水蓋斷了,晚上在客服網頁留言,隔天早上便接到客氣親切的客服信件請我提供地址,再隔兩天零件備品就送到了,在賽前就換新搞定。成本或許不到十元,但客戶感受的價值絕對遠大於此,是一次很棒的客服經驗。


VS2013使用IE10偵錯出現錯誤: 找不到sourcePath屬性

$
0
0

使用VS2013 RTM偵錯網頁出現錯誤:

Unhandled exception at line 77, column 9 in httq://localhost:49498/9c5e62d30ac64961af17a184019bb19e/browserLink

0x800a138f - JavaScript runtime error: Unable to get property 'sourcePath' of undefined or null reference

爬文查到是WebEssentials2013的Bug,而作者在我發現問題3小時前剛完成修正(熱騰騰的,當然還沒Release,要稍等):
[2013-10-24更新] WebEssentials 2013已釋出ver 1.1.1,問題被修正囉。

這才發現WebEssentials是個Open Source Project,而地球另一端的神人正在深夜出擊維持宇宙和平,透過github隨時還能追蹤即時動態。當年用紙卡打洞Coding的程式設計師前輩們,做夢也想不到幾十年後所謂的程式開發是這般光景吧? :P

【.NET隨堂考】object == string比對

$
0
0

來個.NET隨堂測驗,請解釋以下現象:

在以上程式,bool test = d["A"] == "1"所得結果為true,代表二者相等;但由Immediate Window顯示d["A"] == "1"卻是false,為什麼?

首先聲明: d["A"] == "1"的寫法有問題! d的型別為Dictionary<string, object>,故d["A"]所傳回型別為object,對object進行==運算時,等同Object.ReferenceEuals(),只有當前後二者指向同一物件時才會傳回true。由MSDN對於 == 運算子的說明:

    1. 對於預先定義的實值型別 (Value Type),等號比較運算子 (==) 在運算元相等時傳回 true;否則傳回 false。
    2. 對於 string以外的參考型別,若兩個運算元參考到同一物件,== 會傳回 true。
    3. 對於 string 型別,== 會比較字串的值。

程式的原意是要做字串比較,卻忽略了d["A"]傳回的是object,當與"1"比較,內部進行的是Object.ReferenceEquals() [適用上述第2點]。在這個例子中,正確的做法是將d["A"]轉型為string再進行 == 比對,比較的才是二者的字串內容。

事實上,針對這類不適當的 == 使用,Visual Studio會提出警告: (只是警告而非錯誤,程式可編譯、執行)

Possible unintended reference comparison; to get a value comparison, cast the left hand side to type 'string'

很好,我們已指出d["A"] == "1"寫法不當,只有二者指向同一物件時才會得到true。在實測中,Immediate Window檢測d["A"] == "1"為false可以被理解,但test = d["A"] == "1"卻得到true,又是怎麼一回事?

這要從字串常值(String Literal)說起,本草綱目MSDN有記載:

字串常值的型別是 string
每個字串常值並不一定會產生新的字串執行個體 (Instance)。依據字串相等運算子 (章節 7.9.7),當兩個或是多個相等的字串常值出現在相同組件時,這些字串常值便會參考到相同的字串執行個體。

由此可知,程式碼裡出現兩次"1",編譯器會讓兩個"1"字串常值指向同一個字串執行個體,在此情況下,d["A"]與"1"將會指向同一個字串執行個體,Object.ReferenceEquals()傳回true,==運算子的執行結果為true。而在Immediate Window操作所輸入的"1",由於是偵錯期間動態編譯執行,故會指向另外的字串執行個體,故==比對得到false。

解題完畢,大家答對了嗎?

Windows記憶體都用到哪裡去了?

$
0
0

不知大家是否跟我一樣,心中有個懸而未解的謎:

開啟工作管理員,看到記憶體已被用掉14GB,所剩無幾。切到程序列表逐一清點: Visual Studio說它只用了240MB、一堆Chrome個個聲稱自己只耗用不到100MB、IE強調自己克勤克儉只微取50MB、SQL Server大聲喊冤: "不要看我,我超省的! 只用不到75MB,比Chrome還少"... 大伙紛紛跳出撇清。所有程序的耗用量加總起來不到3G,總用量卻高達14G! 短少的部分到哪裡去了? 有程序貪贓枉法,以多報少? 還是某個神祕記憶體大戶私吞巨款潛逃,從程序清單神隱?

以下圖為例,當時總記憶體使用量約14GB,正在執行的108個程序依記憶體用量由大到小排序,前10名加起來約1.2G,第11項-108項由50M開始遞減。好,就算退一萬步,把後面的98項每個都當成50M,也才4.8G,1.2+4.8=6G,總記憶體被用掉14G,相差絕對大於8G,到底被誰偷走?

茶包一哥出身的無敵工具箱,SysInternals,有個破解記憶體之謎的好工具 – RAMMap。執行後只要幾秒,原本疑雲滿天的記憶體舞弊懸案,頓時真相大白,絲絲分明!!

原來,工作管理員(Task Manager)列出的只包含各程序專屬的私用記憶體區,並不包含系統層次的記憶體使用統計,例如: 跨程序的共用記憶體區、檔案系統快取、驅動程式佔用(包含RAMDisk)... 等,都未列在其中,就是數字差異的來源。

在我的案例(如下圖),Process Private(就是工作管理員裡所有程序記憶體用量的加總)約2.3G,Mapped File是檔案快取耗掉6G為最大宗、Metafile也屬檔案快取的一部分,用掉1.3G、而Driver Locked是驅動程式使用部分,由於我切了4G做RAMDisk,加上含其他驅動程式應用共4.8G。Active一欄的加總結果,不多不少,就是14.4G,與當下工作管理員顯示的總記憶體用量完全吻合。(註: Mapped File及Metafile的6G + 1.3G屬快取性質,會在記憶體不足時移給程序使用;當記憶體充足,Windows則會盡量使用快取提高效能)

久懸心中的謎團總算解開,了卻一椿心事... (謎之聲: 有這麼嚴重嗎?)

最後補充,Technet網站有篇介紹文章,詳細說明了上圖各分類及欄位的定義,整理如下供大家參考: (我不是ITPro專家,對OS運作原理了解有限,如有曲解謬誤,請不吝指正)

  • Process Private: 分配給單一Process專用的記憶體
  • Mapped File: 用來儲放檔案內容快取(Cache)的記憶體空間
  • Shared Memory: 標註給多個Process共用的記憶體分頁(Page,記憶體管理單位)
  • Page Table: 用來描述虛擬記憶體位址的分頁表(裡面是一筆一筆的PTE,Page Table Entries)
  • Paged Pool: 允許移至磁碟的核心集區記憶體(Kernal Pool Memory)
  • Nonpaged Pool: 不允許移至磁碟的核心集區記憶體
  • System PTEs: 與I/O空間、核心堆疊、記憶體描述清單等系統分頁相關的PTE
  • Session Private: 登入工作階段相關的記憶體
  • Metafile: 是系統快取的一部份,包含NTFS Metadata(包含MFT及其他NTFS Metadata檔案)。在MFT中,每個檔案屬性記錄佔用1K,而一個檔案至少有一個屬性記錄,再加上其他NTFS Metadata檔,當檔案數眾多,這塊會很快速成長。
  • AWE: 啟用Address Windowing Extension技術所使用的相關記憶體空間(較常應用在SQL或其他DB)
  • Driver Locked: 驅動程式鎖定的實體記憶體。多用於I/O的暫時性小量應用,如果有裝RAMDisk,也會算在這一區。
  • Kernel Stack: 核心執行緒推疊,執行緒愈多,用量愈大。

每項分類都有以下欄位:

  • Active: 正在使用中的實體記憶體分頁(Process Working Set或System Working Set)
  • Standby: 留在實體記憶體但暫不使用的分頁,保留供後續能快速重覆利用
  • Modified: 與Standy類似,但內容被修改過,重覆使用前要先回寫到磁碟機
  • Modified no write: 與Modified類似,但標註為不需回寫到磁碟
  • Transition: 在分類之間轉換的分頁
  • Zeroed: 內容已清空可供使用的分頁,系統剛開機時明顯增加,隨著使用一段時間逐步轉為Standby
  • Free: 可以使用但殘留先前資料的分頁,使用前需先轉為Zeroed
  • Bad: 標註損壞的記憶體

2013貓空半馬~

$
0
0

同一場路跑連續四年參加(1, 2, 3)是什麼感覺? 賽前一如往例找總教練謎之聲討論戰略,他忙著看半澤直樹,連頭也沒抬只丟一句" 隨便你啦! 別吵我"把我打發... 如果這不是"平常心",那什麼是"平常心"呢?

其實也是,算算已跑完十來場全馬,雖然常醉翁之意不在酒,光顧著吃喝賞景,視成績如糞土(實際上我的成績也真的跟糞土所差無幾),但體驗連跑五六個小時的次數一多,對於兩個多小時就結束的半馬就不怎麼放在心上了。

今年路跑忽然夯了起來,一些奇奇怪怪的灑粉螢趴無嘴貓主題路跑都一律被秒殺,往年都採親洽或通訊報名的貓空路跑,或許是怕人數失控,首次限定人數並改採網路線上報名。幸好還不致熱門到一票難求,不然連在"自家後院"辦的路跑都報不到,肯定很悶。

照慣例,我當然又充分運用主場優勢,七點才從家裡晃晃悠悠散步前往會場,連寄物都省了。天氣不錯,多雲偶有陽光,氣溫在20度以下,好天氣,起跑後感覺狀況不差,想想連續幾場全馬都在醉生夢死,墮落夠了,今天只跑半馬,就拼個PB振奮一下吧!

從一起跑便催足油門,甚至好幾次過水站而不入(靠! 我變了),除了在水站喝完水會走一小段緩和,一路都是連跑帶衝! 上坡時也維持小跑步,盡力避免被超車。跑到氣喘吁吁,征服5K長上坡,手表顯示時間為31'52",已打破自己攻上杏花林的最佳記錄。前5K的表現超出預期,後半段更是陷入力破PB的熱血模式無法自拔,沒有閒情逸致拍照,連身旁的其他跑友都沒心思多看,只封閉在墨鏡加MP3圍起的小宇宙裡跑跑跑~

最後5K,歷經一連串高速下坡,右小腿有些緊繃,幾度彷彿已站上抽筋的懸崖邊,靠著暫時減速並調整跑姿才避免爆發。終於撐到政大後門,還剩最後1公里多,一咬牙狠下心,首次完成用跑步征服"後門-蔣公銅像-藝文中心"斷魂坡的壯舉。藝文中心過後還有約400公尺的下坡加繞場200米,右小腿再數次揚言跳崖,幸好仍Hold得住。過終點成績1:57:39,刷新了去年所創下1:58:30的最佳記錄,勇破半馬PB! (灑花)

雖然只快了不到一分鐘,但這就跟Performance Tuning一樣,到達硬體瓶頸後,想再提升一點點都困難重重... 想想,破PB這種壞習慣還是早日戒除為宜 orz

配速分析: 5:25 / 5:18 / 6:36 / 6:55 / 7:09 / 5:57 / 6:13 / 5:22 / 5:31 / 5:43 / 5:09 / 5:07 / 5:10 / 5:17 / 5:20 / 5:25 / 4:39 / 5:00 / 4:50 / 5:16 / 5:31 / 4:26

Visual Studio 2013開啟HTML檔設計檢視

$
0
0

VS 2013 RTM的隔天,我的工作環境就迫不及待全升級成Visual Studio 2013,VS2012的功能都有,再多了BrowserLink、CodeLens等好用功能,Coding樂趣加分不少。

今天倒是發現VS2013有一項改變: 開啟.html檔案時預設不再提供Design/Split/Source三種檢視模式的傳統網頁編輯器,只剩HTML編輯功能。試著改用VS2012開啟.html檔,證實VS2012仍支援設計檢視,此點算是VS2013的行為變更。

說實在話,使用WYSWYG方式在設計檢視拖拉元素固然方便,但背後自動產生的CSS及HTML屬性甚為噁心,不符合當前HTML應力求簡潔乾淨,樣式呈現交由CSS全權操控的設計概念;更何況現今網頁很多需依賴JavaScipt輔助才能正確呈現(例如: Knockout.js、AngularJS),從Visual Studio設計檢視多半已看不出所以然,這大概是VS2013推出BrowerLink,把瀏覽器也整合成IDE一部分的重要理由吧! (關於BrowserLink,Sccot Hanselman有個簡短的Demo影片,值得一看。但需注意,BrowserLink需要Web Application (Web Site Project哭哭),若要讓靜態HTML檔案也能使用BrowserLink,需要額外設定。)

話雖如此,某些時候還是有寫完HTML想快速看看約略樣貌的需求,此時Split分割檢視還挺方便的。找了一下,在Visual Studio 2013仍能用傳統設計檢視編輯.html,方法如下:

1.在Solution Explorer對.html按右鍵選取"Open With..."

2.選取HTML (Web Forms) Editor

3.設計檢視與分割檢視回來囉~

Viewing all 2311 articles
Browse latest View live