2017年4月8日 星期六

將資料存放到 Seesion, 客製化 ModelBinder 來存取

假設有一個購物網站有一個CartController控制器使用Session存取Cart物件的實例
public class CartController : Controller
{
    private IProductRepository repository;
 
    public CartController(IProductRepository repo)
    {
        this.repository = repo;
    }
 
    public RedirectToRouteResult AddToCart(int productId, string returnUrl)
    {
        Product product = repository.Products.FirstOrDefault(p => p.ProductId == productId);
        if (product != null)
        {
            GetCart().AddItem(product, 1);
        }
        return RedirectToAction("Index"new { returnUrl });
    }
 
    public RedirectToRouteResult RemoveFromCart(int productId, string returnUrl)
    {
        Product product = repository.Products.FirstOrDefault(p => p.ProductId == productId);
        if (product != null)
        {
            GetCart().RemoveItem(product);
        }
        return RedirectToAction("Index"new { returnUrl });
    }
 
    private Cart GetCart()
    {
        Cart cart = (Cart)Session["Cart"];
        if (cart == null)
        {
            cart = new Cart();
            Session["Cart"= cart;
        }
        return cart;
    }
}

現在要自定義一個 ModelBinder 來獲取 Session 數據中的 Cart 實例,通過實作 System.Web.Mvc.IModelBinder 介面
public class CartModelBinder : IModelBinder
{
    private const string sessionKey = "Cart";
 
    public object BindModel(
        ControllerContext controllerContext,
        ModelBindingContext bindingContext)
    {
        // 通過 session 取得 Cart
        Cart cart = null;
        if (controllerContext.HttpContext.Session != null)
        {
            cart = (Cart)controllerContext.HttpContext.Session[sessionKey];
        }
 
        // 若 session 中沒有 Cart, 則創建一個
        if (cart == null)
        {
            cart = new Cart();
            if (controllerContext.HttpContext.Session != null)
            {
                controllerContext.HttpContext.Session[sessionKey] = cart;
            }
        }
        return cart;
    }
}

需要告訴 MVC 框架,使用 CartModelBinder 來創建 Cart 實例,在Global.asax 的 Application_Start 方法中設置
public class MvcApplication : System.Web.HttpApplication
{
    protected void Application_Start()
    {
        AreaRegistration.RegisterAllAreas();
        RouteConfig.RegisterRoutes(RouteTable.Routes);
 
        // 告訴 MVC 框架使用 CartModelBinder 類來創建 Cart 實例
        ModelBinders.Binders.Add(typeof(Cart), new CartModelBinder());
    }
}

現在更新 CartController,刪除 GetCart 方法,依靠 CartModelBinder  為 CartController 提供 Cart 物件
public class CartController : Controller
{
    private IProductRepository repository;
 
    public CartController(IProductRepository repo)
    {
        this.repository = repo;
    }
 
    public RedirectToRouteResult AddToCart(Cart cart, int productId, string returnUrl)
    {
        Product product = repository.Products.FirstOrDefault(p => p.ProductId == productId);
        if (product != null)
        {
            cart.AddItem(product, 1);
        }
        return RedirectToAction("Index"new { returnUrl });
    }
 
    public RedirectToRouteResult RemoveFromCart(Cart cart, int productId, string returnUrl)
    {
        Product product = repository.Products.FirstOrDefault(p => p.ProductId == productId);
        if (product != null)
        {
            cart.RemoveItem(product);
        }
        return RedirectToAction("Index"new { returnUrl });
    }
}

用來創建 Cart 物件與 CartController 的邏輯方離開來了,這樣開發者能夠修改存取 Cart 物件的方法而不需要修改 CartController,可以方便對 CartController 做單元測試而不需要模仿大量的 ASP.NET 通道。
[TestMethod]
public void Can_Add_To_Cart()
{
    // Arrange
    Mock<IProductRepository> mock = new Mock<IProductRepository>();
    mock.Setup(m => m.Products).Returns(new Product[]
    {
        new Product{Id=1,Name="P1",Category = "Apples"},
    }.AsQueryable());
 
    Cart cart = new Cart();
 
    CartController target = new CartController(mock.Object);
 
    // Act
    target.AddToCart(cart, 1null);
 
    // Asert
    Assert.AreEqual(1, cart.Products.Count(), 1);
    Assert.AreEqual(1, cart.Products.ToArray()[0].Id);
}

2017年2月18日 星期六

Redux的createStore實作

建立一個計數器範例,點擊頁面會執行累加計數
直接使用 createStore:
import { createStore } from 'redux';

const counter = (state = 0, action) => {
    switch (action.type) {
        case 'INCREMENT':
            return state + 1;
        case 'DECREMENT':
            return state - 1;
        default:
            return state;
    }
}

const store = createStore(counter);
console.log(`initial state : ${store.getState()}`);

store.subscribe(() => {
    document.body.innerHTML = store.getState();
})

document.addEventListener('click', () => {
    store.dispatch({ type: 'INCREMENT' });

});

現在 counter 方法直接沿用,另外建立一個 createStore 方法:
const createStore = (reducer) => {
    // 這個 store 持有 state 變數
    let state;

    const getState = () => state;

    const dispatch = (action) => {

    };

    const subscribe = (listener) => {

    };

    // 回傳的物件被稱為 Redux store
    return { getState, dispatch, subscribe }

};

因為 subscribe() 可以被呼叫很多次,所以需要紀錄這些 listener:
    let listeners = [];

    const subscribe = (listener) => {
        listeners.push(listener)
    };

dispatch() 是唯一可以改變內部 state 的:
    const dispatch = (action) => {
        // 使用當前的 state 和 被 dispatch 的 action 物件當參數呼叫 reducer 計算出新的 state
        state = reducer(state, action);
        // 執行 listeners
        listeners.forEach((listener) => { listener() });
    };

還沒有實作 unsubscribe() 方法,先使用替代方案,在subscribe() 寫一個回傳一個方法:
        // 回傳一個方法
        // 使用方式:
        //     var s1 = store.subscribe(()=>{});
        //     s1(); // unsubscribe
        return () => {
            listeners = listeners.filter(l => l !== listener);
        }

最後在 createStore() 回傳前加入 dispatch({});
為了得到初始 state

createStore 完整範例:
const createStore = (reducer) => {
    let state;
    let listeners = [];

    const getState = () => state;

    const dispatch = (action) => {
        state = reducer(state, action);
        listeners.forEach((listener) => { listener() });
    };

    const subscribe = (listener) => {
        listeners.push(listener);
        return () => {
            listeners = listeners.filter(l => l !== listener);
        }
    };

    dispatch({});

    return { getState, dispatch, subscribe }

};










2017年2月13日 星期一

用最少的套件安裝React應用程式並啟動

1.  建立資料夾,在該資料下開啟終端機並執行:
    $ npm init -y
 
    參考:
    https://docs.npmjs.com/cli/init

2. 安裝 React 套件
    $ npm install --save react react-dom

3. 安裝轉譯套件
    $ npm install --global babel-cli
    $ npm install --save-dev babel-preset-es2015 babel-preset-react

4. 建立 .babelrc
    {
        "presets" : ["es2015","react"]
    }

5. 安裝 webpack
    $ npm install --save-dev webpack

6. 建立 webpack.config.js
var path = require('path');

module.exports = {
  entry: './app/index.js',
  output: {
    filename: 'bundle.js',
    path: path.resolve(__dirname, 'dist')
  }
};    
    參考:https://webpack.js.org/guides/get-started/

7. 執行 babel 和 webpack
    $ babel js/source -d js/build --watch
    $ webpack --config webpack.config.js --watch --colors --progress -d
    --watch : 程式有異動就執行webpack
    --colors : 顯示一些顏色
    --progress : 顯示執行進度
    -d : 加入 Source Map (方便 debug)
    -p : production code (不斷行也不空白的很醜的程式碼)

後續:
想使用babel-loader卻一直失敗,後來發現網路上很多教學文章都是用webpack 1,而我安裝的是2,
配置React的Babel和Webpack2環境:
https://segmentfault.com/a/1190000007000131
https://blog.madewithenvy.com/getting-started-with-webpack-2-ed2b86c68783#.zg5j3c46z
https://www.smashingmagazine.com/2017/02/a-detailed-introduction-to-webpack/?utm_source=javascriptweekly&utm_medium=email
簡單設定:
module.exports = {
    // ...其他設定
    module : {
        rules : [
            {
                test : /\.js$/,
                use : 'babel-loader',
                exclude : /node_modules/
            }
        ]
    }
}
指定路徑下的終端機輸入
$ code .
會運行Visual Studio Code並開啟指定目錄

2016年10月5日 星期三

客製化checkbox樣式

原本的checkbox看起來很小一個,以下為客製化的checkbox:
  • 隱藏原本的checkbox
  • 將label調整成像一個checkbox的外觀
  • 用FontAwesome來顯示打勾

<html>
    <head>
        <link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/font-awesome/4.6.3/css/font-awesome.min.css">
        <style>
            #ckb1{
                display:none;
            }
                #ckb1 + label[for="ckb1"]{
                     border:1px solid gray;
                     width:26px;
                     height:26px;
                     border-radius:4px;
                     display:inline-block;
                 }
                 #ckb1:checked + label[for="ckb1"]:before{
                     color:green;
                     font-family:FontAwesome;
                     content:'\f00c';
                     position:ablolute;
                     font-size:26px;
                     line-height:28px;
                 }
        </style>
    </head>
    <body>
        <input type="checkbox" id="ckb1"/>
        <label for="ckb1"></label>
    </body>
</html>

2016年8月29日 星期一

用簡易的範例快速瞭解 c# delegate 的前世今生

深入研究asp.net core 需要了解 delegate, 所以做筆記複習一下:

using System;
using System.Collections.Generic;
using System.Linq;

namespace ConsoleApp2
{
    // 定義一個委派型別
    public delegate bool Predicate(string s); 

    // 定義一個類別
    public class FruitList 
    {
        private List<string> fruits;

        public FruitList()
        {
            fruits = new List<string> { "Apple", "Banana", "Mango" };
        }
        public string Find(Predicate p)
        {
            for (int i = 0; i < fruits.Count(); i++)
            {
                var f = fruits[i];
                var isMatch = p(f); // 執行委派任務, 等同於  p.Invoke(f)
                if (isMatch)
                {
                    return f;
                }
            }
            return "";
        }
    }
    // demo c# 1.0
    public class Demo1
    {
        public void Run()
        {
            FruitList fruits = new FruitList();
            Predicate p = new Predicate(FindApple); // 建立委派物件. 可以加入多項工作, 例如 : p += new Predicate(FindBanana);
            string f = fruits.Find(p);
            Console.WriteLine(f);
        }

        private bool FindApple(string s)
        {
            return s == "Apple";
        }
    }
    // c# 2.0 : 匿名方法
    public class Demo2
    {
        public void Run()
        {
            FruitList fruits = new FruitList();
            Predicate p1 = FindBanana;  // 編譯器看到變數是委派型別, 便會自動加上 new 
            string f1 = fruits.Find(p1); 
            string f2 = fruits.Find(FindBanana); // 簡化 f1
            Console.WriteLine(f1);

            // 使用匿名方法
            Predicate p3 = delegate (string s) { return s == "Banana"; };
            string f3 = fruits.Find(p3);
            //
            Console.WriteLine(f3);
        }

        private bool FindBanana(string s)
        {
            return s == "Banana";
        }
    }
    // c# 3.0 : Lambda
    public class Demo3
    {
        public void Run()
        {
            FruitList fruits = new FruitList();
            Predicate p1 = (string s) => { return s == "Mango"; }; // Lambda 取代了匿名方法
            Predicate p2 = (string s) => s == "Mango"; // 簡化 p1 寫法
            Predicate p3 = s => s == "Mango"; // 簡化 p2 寫法
            string f1 = fruits.Find(p3);
            string f2 = fruits.Find(s => s == "Mango"); // 簡化 f1 
            Console.WriteLine(f2);
        }
    }

    public class Program
    {
        public static void Main(string[] args)
        {
            var d = new Demo3();
            d.Run();
            Console.ReadKey();
        }
    }
}

2016年8月11日 星期四

drag and drop upload files with asp.net core





前端 HTML : 
<!DOCTYPE html>
<html>
<head>
    <meta charset="utf-8" />
    <title></title>
    <style>
        body {
            font-family"Arial",sans-serif;
        }
 
        .dropzone {
            width300px;
            height300px;
            border2px dashed #ccc;
            color#ccc;
            line-height300px;
            text-aligncenter;
        }
 
            .dropzone.dragover {
                border-color#000;
                color#000;
            }
    </style>
</head>
<body>
    <div id="uploads"></div>
    <div class="dropzone" id="dropzone">Drop files here to append file</div>
    <ul id="files-to-upload"></ul>
    <input type="text" name="tags" id="tags" value="blue,whatever" />
    <input type="text" name="name" id="name" value="name" />
    <button onclick="onUpload()">upload</button>
    <script>
        var dropzone = document.getElementById('dropzone'),
            filesToUpload = document.getElementById('files-to-upload'),
            fileArr = [];
 
        var appendFiles = function (files) {
            console.log(files);
            for (var i = 0; i < files.length; i++) {
                filesToUpload.insertAdjacentHTML('beforeend', '<li>' + files[i].name + '</li>');
                fileArr.push(files[i]);
            }
        };
 
        var onUpload = function () {
            var xhr = new XMLHttpRequest(), formData = new FormData();
            for (var i = 0; i < fileArr.length; i++) {
                formData.append('files', fileArr[i]);
            }
            // append metadata
            formData.append('tags', document.getElementById('tags').value);
            formData.append('name', document.getElementById('name').value);
            console.log(formData);
            //
            xhr.onload = function () { // success
                var data = this.responseText;
                while (filesToUpload.firstChild) { //  faster than "filesToUpload.innerHTML = '';"
                    filesToUpload.removeChild(filesToUpload.firstChild);
                }
                fileArr = []; // empty file list
                console.log(data);
            };
            xhr.open('post', '/Home/Upload');
            xhr.send(formData);
 
        }
 
        dropzone.ondrop = function (e) {
            e.preventDefault(); // 避免瀏覽器開啟圖片
            this.className = 'dropzone';
 
            appendFiles(e.dataTransfer.files);
        };
 
        dropzone.ondragover = function () {
            this.className = 'dropzone dragover';
            return false;
        };
 
        dropzone.ondragleave = function () {
            this.className = 'dropzone';
            return false;
        };
    </script>
</body>
</html>




=========================
後端 ASP.NET Core 

[HttpPost]
public async Task<IActionResult> Upload(ICollection<IFormFile> files, string tags, string name)
{
    foreach (var file in files)
    {
        if (file.Length > 0)
        {
            using (var fileStream = new FileStream(Path.Combine("wwwroot/images", file.FileName), FileMode.Create))
            {
                await file.CopyToAsync(fileStream);
            }
        }
    }
    return Content("A12345678");
}

2016年5月11日 星期三

ASP.NET 5 Self-hosting the application

在 Visual Studio 執行程式時選 web (預設是 IIS Express)

或是

在專案的資料夾([方案名稱]/src/[專案名稱])下開啟命令視窗(按Shift點右鍵會出現選項)

輸入 dnx web 後顯示  Listening on http:localhost:5000

如果要改用別的port, 修改 project.json

dependencies加入 Microsoft.AspNet.Server.WebListener
修改commandsweb(原本預設是:"Microsoft.AspNet.Server.Kestrel")


{
    "dependencies": {        
        "Microsoft.AspNet.Server.WebListener" : "1.0.0-rc-final",
    },
    "commands": {
      "web": "Microsoft.AspNet.Hosting --server Microsoft.AspNet.Server.WebListener --server.urls http://localhost:5001"  
    }
}