2021年7月4日 星期日

Blazor.WebForm.UI - Blazor Component與WebForm Control

StarRatingComponent.razor

@for (int i = 1; i <= 5; i++)
{
    int rating = i;
    <a style="color: @GetColor(rating);font-size:30px;" href="javascript:" @onclick="() => OnChanged(rating)">★</a>
}

@code {
    [Parameter]
    public int CurrentRating { get; set; }

    [Parameter]
    public EventCallback<int> Changed { get; set; }

    private void OnChanged(int rating)
    {
        this.CurrentRating = rating;
        if (Changed.HasDelegate)
        {
            Changed.InvokeAsync(rating);
        }
    }

    private string GetColor(int rating)
    {
        if (rating <= this.CurrentRating)
        {
            return "#f49813";
        }
        else
        {
            return "#cccccc";
        }
    }
}

StarRatingControl.cs

    public class StarRatingControl : UserControl
    {
        public int CurrentRating { get; set; }

        public event EventHandler Changed;

        protected override void OnInit(EventArgs e)
        {
            for (int i = 1; i <= 5; i++)
            {
                LinkButton linkButton = new LinkButton();
                linkButton.Style[HtmlTextWriterStyle.FontSize] = "30px";
                linkButton.Text = "★";
                linkButton.CommandArgument = i.ToString();
                linkButton.Click += this.LinkButton_Click;
                linkButton.PreRender += this.LinkButton_PreRender;
                this.Controls.Add(linkButton);
            }
            base.OnInit(e);
        }

        private void LinkButton_Click(object sender, EventArgs e)
        {
            LinkButton linkButton = (LinkButton)sender;
            int rating = Convert.ToInt32(linkButton.CommandArgument);
            this.CurrentRating = rating;
            if (Changed != null)
            {
                Changed.Invoke(this, e);
            }
        }

        private void LinkButton_PreRender(object sender, EventArgs e)
        {
            LinkButton linkButton = (LinkButton)sender;
            int rating = Convert.ToInt32(linkButton.CommandArgument);
            if (rating <= this.CurrentRating)
            {
                linkButton.Style[HtmlTextWriterStyle.Color] = "#f49813";
            }
            else
            {
                linkButton.Style[HtmlTextWriterStyle.Color] = "#cccccc";
            }
        }
    }

StarRatingControlInComponent.razor

@using System.Web.UI;
@inherits ControlComponent<StarRatingControl>
@this.RenderControl()
@code {
    [Parameter]
    public int CurrentRating
    {
        get
        {
            return this.Control.CurrentRating;
        }
        set
        {
            this.Control.CurrentRating = value;
        }
    }

    [Parameter]
    public EventCallback<int> Changed { get; set; }

    protected override void OnInitialized()
    {
        this.Control.Changed += this.OnChanged;
    }

    private void OnChanged(object sender, EventArgs e)
    {
        if (Changed.HasDelegate)
        {
            Changed.InvokeAsync(this.CurrentRating);
        }
    }
}

StarRatingComponentInControl.cs

    public class StarRatingComponentInControl : UserControl, IRenderComponentControl
    {
        private StarRatingComponent _starRating;

        public int CurrentRating { get; set; }

        public event EventHandler Changed;

        private void OnChanged()
        {
            this.CurrentRating = _starRating.CurrentRating;
            if (Changed != null)
            {
                Changed.Invoke(this, EventArgs.Empty);
            }
        }

        Type IRenderComponentControl.ComponentType
        {
            get
            {
                return typeof(StarRatingComponent);
            }
        }

        void IRenderComponentControl.ComponentReferenceCapture(object componentReference)
        {
            _starRating = (StarRatingComponent)componentReference;
        }

        void IRenderComponentControl.RenderAttributes(IHtmlAttributesWriter writer)
        {
            writer.AddSingleAttribute(nameof(this.CurrentRating), this.CurrentRating);
            writer.AddEventAttribute<int>(nameof(this.Changed), this, this.OnChanged);
        }

        void IRenderComponentControl.RenderContents(IHtmlContentsWriter writer)
        {

        }
    }

StarRatingList.razor

@using System.Web.UI;
@page "/starrating-list"
@inherits ControlComponent
<h3>StarRating List</h3>

@for (int i = 1; i <= 5; i++)
{
    @i
    <StarRatingControlInComponent CurrentRating="i"></StarRatingControlInComponent>
    <br />
}

<hr />

@this.RenderControl()

@code {
    protected override void OnInitialized()
    {
        for (int i = 1; i <= 5; i++)
        {
            this.Controls.Add(new LiteralControl($"{i}"));

            StarRatingComponentInControl starRating = new StarRatingComponentInControl();
            starRating.CurrentRating = i;
            this.Controls.Add(starRating);

            this.Controls.Add(new LiteralControl("<br />"));
        }
    }
}

2020年12月12日 星期六

[Applied]LINQ版本的視窗函數(SQL Window Function)

資料庫SQL中有一些配合Over字句Patition By群組後去處理集合
相當好用的視窗函數,例如編號用的Row_Number、Rank、Dense_Rank等等
似乎可以在LINQ中實現

因此在新版本的Applied套件中新增了此項功能

Nuget網址: https://www.nuget.org/packages/Applied/

底下為示範的程式

public class TestData
{
    public int Year { get; set; }
    public string Name { get; set; }
    public decimal Value { get; set; }
    public decimal Sum { get; set; }
    public decimal Average { get; set; }
    public int RowNumber { get; set; }
    public int Ntile { get; set; }
    public int DenseRank { get; set; }
    public int Rank { get; set; }
    public decimal FirstValue { get; set; }
    public decimal LastValue { get; set; }
    public decimal NthValue { get; set; }
    public decimal Lead { get; set; }
    public decimal Lag { get; set; }
    public decimal CumeDist { get; set; }
    public decimal PercentRank { get; set; }
    public decimal PercentileDisc { get; set; }
    public decimal PercentileCont { get; set; }
    public decimal KeepDenseRankFirst { get; set; }
    public decimal KeepDenseRankLast { get; set; }
    public decimal TeaTime { get; set; }
}
List<TestData> data = new List<TestData>();
data.Add(new TestData() { Year = 2019, Name = "A", Value = 111.1m });
data.Add(new TestData() { Year = 2019, Name = "B", Value = 333.3m });
data.Add(new TestData() { Year = 2019, Name = "C", Value = 333.3m });
data.Add(new TestData() { Year = 2019, Name = "A", Value = 222.2m });
data.Add(new TestData() { Year = 2019, Name = "C", Value = 444.4m });
data.Add(new TestData() { Year = 2019, Name = "A", Value = 222.2m });
data.Add(new TestData() { Year = 2019, Name = "B", Value = 333.3m });
data.Add(new TestData() { Year = 2019, Name = "C", Value = 555.5m });
data.Add(new TestData() { Year = 2020, Name = "A", Value = 111.1m });
data.Add(new TestData() { Year = 2020, Name = "B", Value = 333.3m });
data.Add(new TestData() { Year = 2020, Name = "A", Value = 222.2m });
data.Add(new TestData() { Year = 2020, Name = "C", Value = 333.3m });

data = data.GroupBy(a => new { a.Year }).AsPartition(p => p.OrderBy(a => a.Value).ThenBy(a => a.Name))
.Over(p => p.Sum(a => a.Value), (a, value) => a.Apply(() => new { Sum = value }))
.Over(p => p.Average(a => a.Value), (a, value) => a.Apply(() => new { Average = value }))
.Over(p => p.RowNumber(), (a, value) => a.Apply(() => new { RowNumber = value }))
.Over(p => p.Ntile(2), (a, value) => a.Apply(() => new { Ntile = value }))
.Over(p => p.DenseRank(), (a, value) => a.Apply(() => new { DenseRank = value }))
.Over(p => p.Rank(), (a, value) => a.Apply(() => new { Rank = value }))
.Over(p => p.FirstValue(a => a.Value), (a, value) => a.Apply(() => new { FirstValue = value }))
.Over(p => p.LastValue(a => a.Value), (a, value) => a.Apply(() => new { LastValue = value }))
.Over(p => p.NthValue(a => a.Value, 2), (a, value) => a.Apply(() => new { NthValue = value }))
.Over(p => p.Lead(a => a.Value), (a, value) => a.Apply(() => new { Lead = value }))
.Over(p => p.Lag(a => a.Value), (a, value) => a.Apply(() => new { Lag = value }))
.Over(p => p.CumeDist(), (a, value) => a.Apply(() => new { CumeDist = value }))
.Over(p => p.PercentRank(), (a, value) => a.Apply(() => new { PercentRank = value }))
.Over(p => p.PercentileDisc(0.5m, a => a.Value), (a, value) => a.Apply(() => new { PercentileDisc = value }))
.Over(p => p.PercentileCont(0.5m, a => a.Value), (a, value) => a.Apply(() => new { PercentileCont = value }))
.Over(p => p.KeepDenseRankFirst(g => g.Sum(a => a.Value)), (a, value) => a.Apply(() => new { KeepDenseRankFirst = value }))
.Over(p => p.KeepDenseRankLast(g => g.Sum(a => a.Value)), (a, value) => a.Apply(() => new { KeepDenseRankLast = value }))
.ToList();

執行結果


 

其中Over方法的第一個Func參數有兩種型態

一個是直接由IEnumerable<TSource>集合回傳單一值IElement的彙總方法
可以使用Linq原有的Sum、Average等等去計算集合部分的結果

另一個是IWindowFunctionFactory<TSource>
要回傳IWindowFunction<TSourceBase, IElement>的視窗函數物件,由該物件執行計算

目前已實現有15種一般的Window Function
不過也許可能有特殊需求情況,需要自行去實現自訂的Window Function
可以用IWindowFunction<TSourceBase, IElement>介面去達到自訂功能,如下

public class TeaTime<TSourceBase, IElement> : IWindowFunction<TSourceBase, IElement>
{
    private readonly Func<TSourceBase, IElement> _field;
    public TeaTime(Func<TSourceBase, IElement> field)
    {
        if (field == null)
        {
            throw new ArgumentNullException("field");
        }
        _field = field;
    }
    public IEnumerable<TResult> GetPartitionResults<TSource, TResult>(IRankEnumerable<TSource> elements
        , Func<TSource, IElement, TResult> selector) where TSource : TSourceBase
    {
        foreach (TSource element in elements)
        {
            IElement value = _field(element);
            yield return selector(element, value);
        }
    }
}
public static class MyWindowFunctions
{
    public static IWindowFunction<TSource, IElement> TeaTime<TSource, IElement>(this IWindowFunctionFactory<TSource> factory
        , Func<TSource, IElement> field)
    {
        return new TeaTime<TSource, IElement>(field);
    }
}
data = data.GroupBy(a => new { a.Year }).AsPartition(p => p.OrderBy(a => a.Value).ThenBy(a => a.Name))
.Over(p => p.TeaTime(a => a.Value), (a, value) => a.Apply(() => new { TeaTime = value }))
.ToList();

 

2017年5月31日 星期三

[Applied]簡易的資料對映轉換套件



Applied是一個用於處理DTO屬性值對映複製的.NET元件

概念來自於T-SQL的CROSS APPLY,所以擴充函式才會使用Lambda型態的參數做設計


Nuget網址: https://www.nuget.org/packages/Applied/


使用上非常簡單,首先宣告示範程式用的資料物件類別
    public enum UserEnum
    {
        None,
        User
    }
    [Serializable]
    public class User
    {
        public int UserID { get; set; }
        public string Name { get; set; }
        public DateTime? Time { get; set; }
        public UserEnum Enum { get; set; }
    }
    [Serializable]
    public class UserViewModel
    {
        public int UserID { get; set; }
        public string Name { get; set; }
        public DateTime? Time { get; set; }
        public UserEnum Enum { get; set; }
    }
然後建立一個陣列並對其使用Applied提供的擴充函式
    User[] users = new User[]
    {
        new User() { UserID = 1, Name = "Sam     " },
        new User() { UserID = 2, Name = "John    " }
    };

    users.Apply(a => new { Time = DateTime.Now, Enum = UserEnum.User });
    users.Trim();

這裡Apply()會令陣列的所有項目屬性與參數中給的賦值物件的屬性值設為一樣

只要兩者的屬性名稱相同,型別相同或型別能相互轉換的

Trim()則會除去所有字串屬性值的前後空白

因為函式其實有回傳自身所以也能夠使用類似裝飾者的寫法或者用於Linq查詢語法內
    users.Apply(a => new { Time = DateTime.Now, Enum = UserEnum.User }).Trim();
對於一般方式來說此行的程式則大約需要寫成這樣
    for (int i = 0; i < users.Length; i++)
    {
        users[i].Time = DateTime.Now;
        users[i].Enum = UserEnum.User;
        if (users[i].Name != null)
        {
            users[i].Name = users[i].Name.Trim();
        }
    }
如果每個物件需要設定的屬性越多型別轉換越多寫起來就會越麻煩複雜


而且除了有設定好資料類別的資料物件可以使用這些函式以外

DataTable,DataRow,IDictionary(Key為string)等型態的物件也可以使用這些函式設定或作為賦值的參數

另外還有幾個相互轉換用的函式
    UserViewModel[] vm1 = users.ToDataEnumerable<User, UserViewModel>().ToArray();
    DataTable dt1 = users.ToDataTable();
    Dictionary<string, object>[] ds1 = users.ToDictionaries().ToArray();

    UserViewModel[] vm2 = dt1.ToDataEnumerable<UserViewModel>().ToArray();
    UserViewModel[] vm3 = ds1.ToDataEnumerable<UserViewModel>().ToArray();

    DataTable dt2 = vm1.ToDataTable();
    DataTable dt3 = ds1.ToDataTable();

    Dictionary<string, object>[] ds2 = vm1.ToDictionaries().ToArray();
    Dictionary<string, object>[] ds3 = dt1.ToDictionaries().ToArray();


資料類別陣列,DataTable,Dictionary陣列皆可互相轉換


然後是有關效能的部分

Applied的對映功能主要是以TypeDescriptor的方式來達成
沒有需要程式先預初始化的設計,所以是每次呼叫都會自動的做一次Mapping動作
不過其中還是具有依靠陣列長度來判斷是否轉換為使用Expression Tree以加速處理的機制
(Expression Tree初始較慢執行較快,效益臨界值是訂672,以Array或List使用才能判斷)
固每次任務皆少筆數的話整體效能大致上應該不高,不過差別大概也感覺不到(筆數少)
就看請求任務的總次數多或不多來評估適不適合使用了



新版改以Expression Tree為主,在沒有需要程式先預初始化的設計情況

第一次自動Mapping後存取屬性的Lambda會存入記憶體的字典中下次呼叫就能以類別來快取
https://dotblogs.com.tw/initials/2016/08/20/231753

與這篇文章相同的百萬筆測試,結果為700~800左右,原生的14~17倍

結果為400~500,原生的10倍左右


以上請參考,若有錯誤煩請告知,感謝~

2016年3月12日 星期六

ASP.NET MVVM - 主從式網站資料的Cache服務

主從式網站資料的Cache服務
Excalibur中一直有個能夠利用Cache物件儲存網頁ViewState於後端伺服器中的功能
過去啟用需以Web.config設定Path的方式,但現在的新版本我們有了更快速方便使用的方法
只要直接在Page上加一行程式碼
[PageCacheViewState(true, false)]
public partial class _Default : System.Web.UI.Page
{
    protected void Page_Load(object sender, EventArgs e)
    {

    }
}
當PageCacheViewState使用後網頁前端原始碼
產生到前端的網頁只有一段GUID資料,此功能Cache Timeout預設留存40分鐘(可web.config設定)
讓ASPX網頁的ViewState資料不要往返前後端在PostBack時消耗頻寬傳送,可大幅提升網頁效能速度
並且在新版本Excalibur使用Cache的方式也有項很特別的變更,就是其中附加了一隻新的Cache服務程式
只要當第一次Cache時類別庫會自動的在伺服器啟動一個Excalibur.Scabbard背景行程
此行程的功用就是專門用來儲存Cache的,它會以TCP/IP的方式和持續和網站連線,將序列化後的資料儲存於其中
為何我們需要特地為Cache這麼做呢?因為IIS的機制本身會定期回收網站行程,而且IIS網站可以設定多行程並行
原本ASP.NET提供的Cache卻只能依附在單一網站行程內,只要工作區關閉重啟Cache資料必定會消失
若是設定多行程的網站,行程之間使用的Cache也不會是同一份資料
更有可能因為線上網站是有使用負載平衡,多台主機的資料根本沒辦法跨主機行程共用
預設的情況我們不需任何設定就會為一個網站開啟一個Excalibur.Scabbard行程
若需要跨網站或主機間使用,只要於Web.config中指定IP和Port即可
<configuration>
  <configSections>
    <section name="extensions" type="CachePage.ExtensionsConfigSection"/>
  </configSections>
  <extensions>
    <cacheService>
      <netServer hostName="127.0.0.1" port="6143"></netServer>
      <!--<netClient hostName="127.0.0.1" port="6143"></netClient>-->
    </cacheService>
  </extensions>
</configuration>
很簡單的在主網站的Server用netServer設定,其他網站Client用netClient設定
然後這個Cache服務不僅僅只能用來暫存ViewState,Excalibur也提供了可以呼叫的方法

只要將原始的Cache物件使用方式改成有Item的擴充函式就可以了
ps. Excalibur.Scabbard行程關閉條件是在IIS關閉網站後,沒有任何網站與它連線的5分鐘左右自動關閉

2013年10月6日 星期日

ASP.NET MVVM - 兩個Extensions UserControl互動與相互傳值的方法

如果常在WebForm應用程式中切割一堆UserControl使用
難免會碰上互動傳值的問題,而且解法應該不少
普通情況只要頁面上有Register過ascx路徑都可以取得類別型態與Property/Event
然後也能利用FindControl和Interface來處理...等等
但若是兩個UserControl放置在千里之遙
甚至不在氣泡事件OnBubbleEvent能解決的樣版關係的情況下
可以試試以下設計的方法






SendMessage方法傳送的第一個參數即MessageNotify事件EventArgs的CommandName
第二個參數即事件EventArgs的CommandArgument
MessageNotify中的sender則是發送訊息的UserControl物件

這裡呼叫SendMessage會對Page內所有Extensions UserControl註冊的MessageNotify事件進行觸發
但是不會觸發發送訊息的UserControl自己本身註冊的MessageNotify事件
所以也能在MessageNotify事件時呼叫SendMessage傳送其他訊息出去

 (MVVM 1.1.1.8版本以上加入)

後記:在更新版本中加入了Attribute的使用方法,示例中的UserControl2可改寫成這樣

相當於直接標註方法為可讓其他控制項以SendMessage公開呼叫的意思
呼叫的Command為指定的方法名稱,Argument為參數

2013年9月25日 星期三

ASP.NET MVVM - ExtensionsControl (2)

這次MVVM更新到1.1.1.5版了,那這裡要介紹的新控制項是RouteManager跟HolderSource
顧名思義第一個控制項八成是用來搞Url的Route用的
第二個也一定和PlaceHolder用來動態載入控制項的功能有關

首先要使用RouteManager因為要用到ASP.NET 4.0才加入的Route功能,需要在Global.asax中設定
假設網站首頁的路徑是"~/Default.aspx"那麼Application_Start中就加入以下程式


前兩行是排除不需改變Route規則的Url,然後只要將"~/Default.aspx"
傳入RouteManager的Configure方法即可
接著就新增首頁的Default.aspx檔案

在Default.aspx的頁面中加入RouteManager與HolderSource控制項
其中HolderSource可同時放入多個並且可以在部分HolderSource的HolderTemplate樣板中
能再放入一層的單個RouteManager加多個HolderSource的組合,之後數層的規則一樣
例如就像下面這樣具有到第三層的樹狀排列結構


這樣就完成可以執行測試了

RouteManager中只有一個DefaultLoadName屬性可以用來選擇RouteValue
不符時預設顯示的HolderSource的ID
然後HolderSource的ID則就是被用來與Url之中當RouteValue比對
HolderSourceID與RouteValue相符就會顯示HolderTemplate的內容
比如說瀏覽器網址是"http://localhost:25179/Index/"
在上面的例子就會呈現Index這個HolderSource的樣板內容
若網址導到是"http://localhost:25179/Home/"
Index的樣板就不會呈現而改成呈現Home的樣版內容
如果網址後面多加一個目錄變成"http://localhost:25179/Home/Page1/"
那麼除了呈現Home的樣版內容外還會讓Home中Page1的樣版內容也一起呈現出來
依此類推,這樣我們就可以利用Route控制網頁中控制項的呈現





HolderSource也有個IsLoad屬性預設值是false用來控制樣版呈現,當樣版內容不呈現時
不管裡面被放什麼控制項都不會被載入跟進入生命周期事件那麻煩的ViewState也就不會產生了
RouteManager的工作就只是用Route物件比對Url,先算出自己在哪一層
再找出應該要用來呈現的HolderSource將IsLoad變成true而已
而且因為樣板裡面也能夠放入UserControl那些
所以也可以視情況在UserControl中選擇再放入RouteManager與HolderSource進去
只要總共不要超過10個的RouteValue就應該沒有問題了吧

2013年9月15日 星期日

技術的堅持和原則重要嗎?

關於到底重不重要這個問題我覺得真的沒有很重要耶
因為我覺得就算說多重要很多時候也只是被拿來說說而已
否則我們也不會有這麼多有關技術債留下的難題了
例如我就遇到過的:
a.一個網頁表單的aspx.cs會出現包山包海的數千行程式碼(神物件)
b.單一個存表單資料的資料表欄位能多達上百欄(反正規化?)
c.某個App_Code中的套印類別居然用上三維陣列(可能在實做什麼演算法吧)
d.網站首頁被放入複雜sql統計無cache直到有天流量尖峰使用者無法登入
e.網站登入資訊頁在Page_Load直接呼叫外站服務無例外處理讓網站隨外站一起掛
f.到處都是的SQL Injection弱點(真的可以執行sp_MsForEachTable逞罰)
g.同事網站的登入資訊居然用靜態變數儲存,上線自己還抓不到BUG
h.還有更多萬能的Session在控制項的事件中設定與讀取
...
還有許多,這些要列是列不完的
有句話說"知識是有限的,只有愚蠢才是無限的"大概就是形容這些。
但是我們也不能否認在獲得知識前無知的自己大概也曾經犯下過錯(或臨時的發蠢狀態XD)
差別在每個人蠢的時間長度不同,有些人真的是橫寫的阿拉伯數字八阿

可是這邊就產生了更奇怪的問題,而且對我們也許可能更加的重要
"解決這些技術債的問題算聰明還是愚蠢呢?"
我認為會有這些問題時表示技術的重要性已經有很大程度的被無視了
這時候應該要考慮的是"你自己的位置"
Position Yourself(http://st-threath.blogspot.tw/2013/09/position-yourself.html)
其實我是想分享這篇文章
當技術在你身上,而你在這個地方(黑人騎馬真得很奇怪?...)
會不會使你變得強勢還是繼續的弱勢你真的要想清楚

以前我曾經有件任務要去修改一個ClassLibrary1.exe的專案
這隻是個定期從外站服務撈取資料然後轉換存進系統資料庫的排程程式而已
需要我在裡面加上在存進資料的同時過濾出可疑資料自動發信email給使用者的功能
沒錯...這個專案名稱就是ClassLibrary1,新增類別庫專案時的預設名子(然後專案屬性被改成執行檔)
可想而知裡面的程式碼根本無法維護,要不就是我能力太差吧
沒有辦法,那時我就只好花了超出預定一點的時間去整個重寫這隻程式(不只是換個好名子啦XD)
後來也只是被主管問到是不是程式重寫了而已,感覺不出很在意為什麼的樣子
不過我也沒去多想...當然後來在那間公司還是有發生很多其他事情
最後也離開了那裡,和那間公司最後的關係就是互相的逞罰吧...
至於現在的我多少了解到關於真正的問題是出在哪裡

我們不應該留在視對技術的堅持和原則為愚蠢的地方
更重要的是要能更快的去查覺自己的位置、關於技術是被怎樣看待的
自己的機會會在哪裡、是否有什麼方法可以改變它,那些多少都是有跡可循的(不知道就google公司名稱阿XD)
又或者你的判斷是因為在其他地方有利所以可以讓步無所謂也罷
最怕的是什麼都沒想,沒發現自己處在不允許任何堅持的劣勢
那時再問對技術的原則與堅持重要嗎?
吼,我自己舉自己的例子都覺得有點丟臉了啦>///<