2015年11月2日 星期一

angularjs 1.x upload image and preview

<html>
<head>
    <meta charset="utf-8" />
    <title></title>
    <script src="scripts/angular.min.js"></script>
    <script>
        var app = angular.module('myApp', []);
 
        app.controller('homeCtrl', function ($scope, $http) {
 
            $scope.imageSources = [];
            //  預覽
            $scope.onPreviewImage = function (files) {
                var i = 0, length = files.length, reader;
                $scope.imageSources = [];
                for (= 0; i < length; i++) {
                    reader = new FileReader();
                    reader.onload = function (event) {
                        $scope.imageSources.push(event.target.result);
                        $scope.$apply();
                    };
                    reader.readAsDataURL(files[i]);
                }
            };
            // 上傳檔案
            $scope.onUploadFile = function (files) {
                var fd = new FormData(), length = files.length, i;
                for (= 0; i < length; i++) {
                    fd.append('files', files[i]); // 後端(MVC)接收的變數為 IEnumerable<HttpPostedFileBase> files
                }
                /* undefined Content-Type and transformRequest: angular.identity that
                 * give the $http the ability to choose the right Content-Type
                 * and manage the boundary needed when handling multipart data.
                 */
                $http.post('Upload', fd, { // 上傳
                    withCredentials: true,
                    headers: { 'Content-Type': undefined },
                    transformRequest: angular.identity
                });
            };
        });
    </script>
</head>
<body ng-app="myApp" ng-controller="homeCtrl">
    <input type="file" multiple onchange="angular.element(this).scope().onPreviewImage(this.files)" accept="image/*" />
    <img ng-repeat="img in imageSources" ng-src="{{img}}" />
    <button ng-click="onUploadFile()">上傳</button>
</body>
</html>


Copy and Paste Formatting with Visual Studio’s Dark Them :
https://codinglifestyle.wordpress.com/2013/05/17/copy-and-paste-formatting-with-visual-studios-dark-theme/

2015年9月2日 星期三

在 Web Api 自訂 ClaimsIdentity 和在取 token 的回傳 josn 中新增資訊

新增一個 Web Api 專案, 查看 App_Start/Startup.Auth.cs 的這個方法 :
public void ConfigureAuth(IAppBuilder app)
{
    // 設定資料庫內容和使用者管理員以針對每個要求使用單一執行個體
    app.CreatePerOwinContext(ApplicationDbContext.Create);
    app.CreatePerOwinContext<ApplicationUserManager>(ApplicationUserManager.Create);
 
    // 讓應用程式使用 Cookie 儲存已登入使用者的資訊
    // 並使用 Cookie 暫時儲存使用者利用協力廠商登入提供者登入的相關資訊;
    app.UseCookieAuthentication(new CookieAuthenticationOptions());
    app.UseExternalSignInCookie(DefaultAuthenticationTypes.ExternalCookie);
 
    // 設定 OAuth 基礎流程的應用程式
    PublicClientId = "self";
    OAuthOptions = new OAuthAuthorizationServerOptions
    {
        TokenEndpointPath = new PathString("/Token"),
        Provider = new ApplicationOAuthProvider(PublicClientId),
        AuthorizeEndpointPath = new PathString("/api/Account/ExternalLogin"),
        AccessTokenExpireTimeSpan = TimeSpan.FromDays(14),
        // 在生產模式中設定 AllowInsecureHttp = false
        AllowInsecureHttp = true
    };
 
    // 讓應用程式使用 Bearer 權杖驗證使用者
    app.UseOAuthBearerTokens(OAuthOptions);
}
得知 OAuthAuthorizationServerOptions.Provider 使用 ApplicationOAuthProvider, 想要在取 token 所回傳的 json 中加入其他資料(例如 name 和 city), 因此針對他的 GrantResourceOwnerCredentials 方法進行修改 :
public override async Task GrantResourceOwnerCredentials(OAuthGrantResourceOwnerCredentialsContext context)
{
    await Task.Run(() =>
    {
        // 自行驗證
        if (context.UserName == "ian" && context.Password == "1111")
        {
            // 驗證通過
 
            // 建立一個 ClaimsIdentity
            var identity = new ClaimsIdentity(context.Options.AuthenticationType);
            // 加入一些自行命名且好讀的 Claim
            identity.AddClaim(new Claim("name""Ian"));
            identity.AddClaim(new Claim("city""Tainan"));
            // 建立一個 AuthenticationProperties
            var p1 = new Dictionary<stringstring>
            {
                {"name","Ian"}, // 顯示於回傳的json中
                {"city","Tainan"}
            };
            AuthenticationProperties properties = new AuthenticationProperties(p1);
            // 使用 ClaimsIdentity 和 AuthenticationProperties 來產生一個 AuthenticationTicket
            AuthenticationTicket ticket = new AuthenticationTicket(identity, properties);
            // 替換此內容上的票證資訊,並讓其由應用程式驗證。 呼叫之後,IsValidated 為 true 且 HasError 為 false。
            context.Validated(ticket);
            context.Request.Context.Authentication.SignIn(identity);
        }
        else
        {
            context.SetError("invalid_grant""使用者名稱或密碼不正確。");
            return;
        }
    });
}

測試 :
  1. POST /token
  2. form data :
  3. username:
    ian
  4. password:
    1111
  5. grant_type:
    password
使用 jQuery.ajax 取 Token :
$.ajax({
    url: '/token',
    data: { username: username, password: password, grant_type: 'password' },
    type: 'POST',
    success: function (data) {
        console.log(data);
        $('#token').val(data.access_token);
    }
 
});

結果 :

{"access_token":"F5PU_EhpbLl7aw_EwlTuP8unNXc4L9olOlqbuan0oQbjPyh-u5iEUjQnRcs7AkV6ia7clMPn8JyZZxNucD5mP_vUTvmeGjDGZJI33qzWzehGP4xQr5HCMQ0EtaCBi7pxq0WttOtLYumoZNXmBDnRWqTtn3s7iBszewS1IHb__J2-zd1nzVmT7VKOVe_GFQhKCB_cXMqCPyfPaERLrzBYjT3ju3RYIrDn1m-ZuaLXwVM","token_type":"bearer","expires_in":1209599,"name":"Ian","city":"Tainan",".issued":"Wed, 02 Sep 2015 07:33:46 GMT",".expires":"Wed, 16 Sep 2015 07:33:46 GMT"}

name 和 city 被加入到回傳的 json 中

在後端程式中使用 ClaimsIdentity 中的資料, 例如取得使用者的 city :
[Authorize]
public class UserCityController : ApiController
{
    public string Get()
    {
        var identity = User.Identity as ClaimsIdentity;
        var city = (identity.Claims).Where(x => x.Type == "city").Select(x => x.Value).FirstOrDefault();
        return city;
    }
}

前端使用 jQuery 撈取 :
$.ajax({
    url: '/api/usercity',
    type: 'GET',
    headers: {
        'Authorization': 'Bearer ' + $('#token').val()
    },
    success: function (data) {
        $('#user-city').text(data);
    }
 
});
Authorization : Bearer {token}






2015年8月14日 星期五

angular directive 連動選單

<html lang="en" xmlns="http://www.w3.org/1999/xhtml">
<head>
    <meta charset="utf-8" />
    <title>angular directive 連動選單</title>
    <script src="../Scripts/angular.min.js"></script>
    <script>
        var app = angular.module('myApp', []);
 
        // directive
        app.directive('categorySelection', function ($http) {
            var _template = '<div>' +
                             // 第一分類
                                '<select class="category-first form-control" ng-model="selectedCategory.first">' +
                                    '<option value="0">--第一分類--</option>' +
                                    '<option ng-repeat="f in categorySelection.firsts" value="{{f.Id}}">{{f.Text}}</option>' +
                                '</select>' +
                                // 第二分類
                                '<select class="category-second form-control" ng-model="selectedCategory.second">' +
                                    '<option value="0">--第二分類--</option>' +
                                     '<option ng-repeat="s in categorySelection.seconds" value="{{s.Id}}">{{s.Text}}</option>' +
                                '</select>' +
                                // 第三分類
                                '<select class="category-third form-control" ng-model="selectedCategory.third">' +
                                    '<option value="0">--第三分類--</option>' +
                                    '<option ng-repeat="t in categorySelection.thirds" value="{{t.Id}}">{{t.Text}}</option>' +
                                '</select>' +
                            '</div>';
 
            return {
                restrict: 'E',
                scope: {
                    first: '=', // 第一分類, 用"等於"符號做雙向綁定
                    second: '=', // 第二分類
                    third: '=' // 第三分類
                },
                controller: function ($scope) {
                    // 已經被選取的分類
                    $scope.selectedCategory = {
                        first: 0,
                        second: 0,
                        third: 0
                    };
                    // 分類選項
                    $scope.categorySelection = {};
                    $scope.categorySelection.firsts = [];
                    $scope.categorySelection.seconds = [];
                    $scope.categorySelection.thirds = [];
                },
                replace: true,
                template: _template,
                link: function (scope) {
                    // 取第一類選項資料
                    refeshCategorySelection('first');
 
                    // 第一分類選項異動, 更新第二分類選項
                    scope.$watch("selectedCategory.first", function () {
                        scope.first = parseInt(scope.selectedCategory.first, 10);
 
                        if (scope.first !== 0) {
                            refeshCategorySelection('second', scope.first);
                        } else {
                            scope.categorySelection.seconds = [];
                            scope.categorySelection.thirds = [];
                        }
                    });
                    // 第二分類選項異動, 更新第三分類選項
                    scope.$watch("selectedCategory.second", function () {
                        scope.second = parseInt(scope.selectedCategory.second, 10);
 
                        if (scope.second !== 0) {
                            refeshCategorySelection('third', scope.second);
                        } else {
                            scope.categorySelection.thirds = [];
                        }
                    });
                    // 第三分類選項異動
                    scope.$watch("selectedCategory.third", function () {
                        scope.third = parseInt(scope.selectedCategory.third, 10);
                    });
                    // 更新選單
                    function refeshCategorySelection(type, id) {
                        $http.get('/Home/GetData?id=' + id).success(function (data) {
                            switch (type) {
                                case 'first':
                                    scope.categorySelection.firsts = data;
                                    scope.categorySelection.seconds = [];  // 清空第二分類選單 (template上剩下預設的"--第二分類--"選項)
                                    scope.categorySelection.thirds = [];
                                    scope.selectedCategory.first = 0; // 設定館分類被選取值 (讓被選取值回到"--第一分類--"選項上)
                                    scope.selectedCategory.second = 0;
                                    scope.selectedCategory.third = 0;
                                    break;
                                case 'second':
                                    scope.categorySelection.seconds = data;
                                    scope.categorySelection.thirds = [];
                                    scope.selectedCategory.second = 0;
                                    scope.selectedCategory.third = 0;
                                    break;
                                case 'third':
                                    scope.categorySelection.thirds = data;
                                    scope.selectedCategory.third = 0;
                                    break;
                                default:
                                    break;
                            }
                        });
 
                    }
                }
            }
        });
 
        // controller
        app.controller('homeCtrl', function ($scope) {
            $scope.query = {};
            $scope.query.first = 0;
            $scope.query.second = 0;
            $scope.query.third = 0;
 
            $scope.onSend = function () {
                console.log($scope.query);
            };
        });
    </script>
</head>
<body ng-app="myApp" ng-controller="homeCtrl">
    <category-selection first="query.first" second="query.second" third="query.third"></category-selection><button ng-click="onSend()">Send</button>
</body>
</html>

2015年5月24日 星期日

NSDate

Apple官方文件說明NSDate:
https://developer.apple.com/library/ios/documentation/Cocoa/Reference/Foundation/Classes/NSDate_Class/index.html


// 當地日期時間,沒有秒。ex : "May 25, 2015, 11:30 AM"

let date = NSDate() 

// 格式化
var formatter = NSDateFormatter()
formatter.dateFormat = "yyyy-MM-dd HH:mm:ss ZZZ"

let defaultTimeZonStr = formatter.stringFromDate(date) // 同樣的日期時間,但是有秒。ex : "2015-05-25 11:34:21 +0800"

formatter.timeZone = NSTimeZone(abbreviation: "UTC")
let utcTimeZoneStr = formatter.stringFromDate(date) // 同樣的日期時間,但是在UTC。ex : "2015-05-25 03:34:21 +0000"

// 設定日期
var endDate = NSDate().dateByAddingTimeInterval(30 * 60) // 30 minutes from current time

// 日期比較
var dateComparisionResult:NSComparisonResult = NSDate().compare(endDate)
if dateComparisionResult == NSComparisonResult.OrderedAscending
{
    println("當前日期時間比 end date .") // 這個範例中,這個結果會被打印出來
}
else if dateComparisionResult == NSComparisonResult.OrderedDescending
{
    println("當前日期時間比 end date .")
}
else if dateComparisionResult == NSComparisonResult.OrderedSame
{
    println("當前日期時間與 end date 相同.")
}

// iOS 8.0+ NSCalendar 方法
var cal = NSCalendar.currentCalendar()

cal.startOfDayForDate(endDate) // 忽略時間元素,ex : "May 25, 2015, 12:00 AM"
cal.isDateInToday(endDate) // 指定日期是否為今天。其他的還有 
cal.isDateInTomorrow(endDate)
cal.isDate(NSDate(), inSameDayAsDate: endDate) // 比對兩個日期是否相同

// 字串轉為日期
var dataString = "April 1, 2015" as String
var dateFormatter = NSDateFormatter()
dateFormatter.dateFormat = "MM-dd-yyyy"
dateFormatter.timeZone = NSTimeZone.localTimeZone()
let dateValue = dateFormatter.dateFromString(dataString) as NSDate!

println(dateValue) // ex : "Apr 1, 2015, 12:00 AM"

格式化:


























類似“/Date(1427909016000-0800)”格式的timestamp,要轉成NSDate:

func timestampToNSDate(jsonDate: String) -> NSDate? {
    let prefix = "/Date("
    let suffix = ")/"
    let scanner = NSScanner(string: jsonDate)
    
    // Check prefix:
    if scanner.scanString(prefix, intoString: nil) {
        
        // Read milliseconds part:
        var milliseconds : Int64 = 0
        if scanner.scanLongLong(&milliseconds) {
            // Milliseconds to seconds:
            var timeStamp = NSTimeInterval(milliseconds)/1000.0
            
            // Read optional timezone part:
            var timeZoneOffset : Int = 0
            if scanner.scanInteger(&timeZoneOffset) {
                let hours = timeZoneOffset / 100
                let minutes = timeZoneOffset % 100
                // Adjust timestamp according to timezone:
                timeStamp += NSTimeInterval(3600 * hours + 60 * minutes)
            }
            
            // Check suffix:
            if scanner.scanString(suffix, intoString: nil) {
                // Success! Create NSDate and return.
                return NSDate(timeIntervalSince1970: timeStamp)
            }
        }
    }
    return nil
}
timestampToNSDate("/Date(1427909016000-0800)/")


2015年1月29日 星期四

閉包 Closures - 高階函式 Higher-order functions

func connect(str1 : String, str2 : String) -> String {
    return str1 + " " + str2
}
connect("hello","world") // "hello world"

func say(name : String, function : (String, String) -> String) -> String{
    let greet = "你好"
    var str = function(name, greet)
    return str
}

// 1. 函數被當成變數傳入另一個函數
var result = say("Ian",connect)
println(result) // "Ian 你好"

// 2. 閉包運算式(Closure Expressions
// connect函數的名稱用函數型態取代,in關鍵字之後為程式碼內容。
result = say("Peter", { (str1:String, str2:String) -> String in
    return str1 + " " + str2
})
println(result) // "Peter 你好"

// 3. 根據上下文推斷型態(Inferring Type From Context
result = say("Alex", { str1, str2 in
    return str1 + " " + str2
})
println(result)// "Alex 你好"

// 4. 閉包單行表示式隱含返回(Implicit Return From Single-Expression Clossures
// 拿掉return關鍵字
result = say("Jay", { str1, str2 in str1 + " " + str2 })
println(result) // "Jay 你好"

// 5. 參數名稱縮寫(Shorthand Argument Names
// 直接通過$0,$1,$2...來順序引用閉包的參數
result = say("Alice", { $0 + " " + $1 })
println(result) // "Alice 你好"

// 6. 運算符函數(Operator Functions
// 簡單地傳遞一個加號,Swift可以自動推斷出我們想使用加號的字串函數操作。
result = say("Bob", + )
println(result) // "Bob 你好"

// 尾隨閉包(Trailing Closures
// 把最後一個參數(閉包函數)搬出參數列括弧,整個大括弧搬出小括弧之外。當閉包內容冗長的時候,適合這樣表達。
result = say("Tim"){
    (str1 : String, str2 : String) -> String in return str1 + str2
}
println(result) // "Tim 你好"
======================================

使用 Javascript 的範例,改寫成 Swift 版
http://ianworkshop.blogspot.tw/2014/03/blog-post.html

高階函示 : 
1. 把其他函式當作引數 (callback function / 回呼函式, 即呼叫時傳入她, 然後她再呼叫回來)
2. 會回傳函式

  • 使用標準程式庫原本的方法


// 將陣列中的數值由小到大重新排列
var nums = [1,3,5,7,9,8,6,4,2]

// 2. 閉包運算式
nums.sort({ (n1:Int, n2:Int) -> Bool in
    n1 < n2
})
println(nums)

// 3. 推斷型態
nums.sort({ n1, n2 in
    return n1 < n2
})

// 4. 隱含返回 (拿掉return關鍵字)
nums.sort({ n1, n2 in n1 < n2 })

// 5. 參數名稱縮寫
nums.sort({ $0 < $1 })

// 6. 運算符函數
nums.sort( < )

// 7. 尾隨閉包
nums.sort{ (n1:Int, n2:Int) -> Bool in
    n1 < n2
}

// 尾隨閉包 + 參數名稱縮寫

nums.sort{ $0 < $1 }

======
// 將陣列中的字串都轉為大寫
var names = ["Ian","Peter","Kevin"]
// 尾隨閉包 + 參數名稱縮寫

names = names.map { $0.uppercaseString }
println(names) // "[IAN, PETER, KEVIN]"



  • 自訂高階函式
// 依照條件建立字串
func builderString(n:Int, callback:(counting:Int) -> String ) -> String {
    var result = "";
    for i in 0...n {
        result = result + callback(counting: i)
    }
    return result
}

// 建立 1~9
var digits_1 = builderString(8, { (counting) -> String in
    return "\(counting+1)"
})

var digits_2 = builderString(8) { "\($0+1)"}