2021年12月22日 星期三

從datasource 產生datatable 並刪除不要的資料

DataView view = (DataView)SqlDataSource1.Select(new DataSourceSelectArguments());
DataTable dt = view.ToTable();
for (int i = dt.Rows.Count - 1; i >= 0; i--)
{
DataRow dr = dt.Rows[i];
if (...) dr.Delete();
}
dt.AcceptChanges();

ValueTuple default 判斷方式

請勿直接用 xxx == default
改用以下方式
xxx == default(ValueTuple<int, string>)

SQL Server System-Versioned Temporal Tables & cdc

於 table 增加兩個欄位來啟用內建的table row log 機制 (僅限 2016 以上版本)

ALTER TABLE dbo.AWBuildVersion ADD
  [TimeStart] DATETIME2(0)  GENERATED ALWAYS AS ROW START NOT NULL CONSTRAINT DFT_AWBuildVersion_TimeStart DEFAULT ('19000101'),
  [TimeEnd] DATETIME2(0) GENERATED ALWAYS AS ROW END NOT NULL CONSTRAINT DFT_AWBuildVersion_TimeEnd DEFAULT ('99991231 23:59:59'),
  PERIOD FOR SYSTEM_TIME ([TimeStart], [TimeEnd]);
 ALTER TABLE dbo.AWBuildVersion
 DROP CONSTRAINT DFT_AWBuildVersion_TimeStart, DFT_AWBuildVersion_TimeEnd;
 
ALTER TABLE dbo.AWBuildVersion  SET ( SYSTEM_VERSIONING = ON ( HISTORY_TABLE = dbo.AWBuildVersion_History ) );

第一次修改資料後就會自動產生history table,裡面會有修改前的資料,若有新增欄位等行為也會自動同步
















※後續若要調整table scheme 只能透過指令,已經不能再用設計功能處理
※若要知道每個版本是誰修改,要另外增加修改者的欄位,每次更新時記錄更新人員

若是 2016 以前版本,可以使用 cdc (異動資料擷取)

啟用資料庫cdc
sys.sp_cdc_enable_db

啟用資料表cdc
EXECUTE sys.sp_cdc_enable_table
    @source_schema = N'dbo',
    @source_name = N'table1',
    @role_name = NULL;

之後異動會於系統資料表出現 dbo_table1_CT,每次異動產生兩筆row
__$operation=1 表示刪除
__$operation=2 表示新增
__$operation=3 表示異動前
__$operation=4 表示異動後

若有更改資料表結構,例如增加欄位,必須重新產生log table,否則不會記錄到新欄位的變化,使用以下指令 disable cdc (會自動 drop log table)
EXEC sys.sp_cdc_help_change_data_capture @source_schema = 'dbo', @source_name = 'student';
EXEC sys.sp_cdc_disable_table @source_schema = N'dbo', @source_name   = N'student', @capture_instance = xxx;

取得異動時間
select cdc.lsn_time_mapping.tran_begin_time,...
from xxx
INNER JOIN cdc.lsn_time_mapping ON xxx.__$start_lsn = cdc.lsn_time_mapping.start_lsn

異動資料預設保存三天,若要調整天數使用以下指令 (每個資料庫各別設定,單位是分鐘,43200=30天)
EXEC sys.sp_cdc_change_job @job_type=N'Cleanup' ,@retention=43200

2021年12月17日 星期五

asp.net mvc api 學習心得

網址指定action name 呼叫對應action
WebApiConfig.cs routeTemplate 加入 {action}
public static void Register(HttpConfiguration config)
{
config.Routes.MapHttpRoute(
name: "DefaultApi",
routeTemplate: "api/{controller}/{action}/{id}", 
defaults: new { id = RouteParameter.Optional }
);

Authorize 導入JWT
●安裝 jwt.net
●新增 JwtAuthorizeAttribute class
using System.Net.Http; // to use GetRequestContext()
public class JwtAuthorizeAttribute : AuthorizeAttribute
{
protected override bool IsAuthorized(HttpActionContext actionContext)
{
var authorization = actionContext.Request.Headers.Authorization;
if (authorization != null && authorization.Scheme == "Bearer")
{
var token = authorization.Parameter;
var secret = System.Web.Configuration.WebConfigurationManager.AppSettings["JWTSecret"];
var claims = JwtBuilder.Create()
.WithAlgorithm(new HMACSHA256Algorithm()) // symmetric
.WithSecret(secret)
.MustVerifySignature()
.Decode<IDictionary<string, object>>(token).Select(a => new Claim(a.Key.ToLower() == "name" ? ClaimTypes.Name : a.Key.ToLower() == "role" ? ClaimTypes.Role : a.Key, a.Value.ToString()) { });
var identity = new ClaimsIdentity(claims, "JWT");
var principal = new ClaimsPrincipal(identity);
actionContext.Request.GetRequestContext().Principal = principal;
}
return base.IsAuthorized(actionContext);
}
}
●webapiconfig.cs 加入
config.Filters.Add(new JwtAuthorizeAttribute());

被呼叫端Controller
public class p
{
    public string lineid { get; set; }
}
[JwtAuthorize]
public class xxxController : ApiController
{
[HttpPost]
public IHttpActionResult aaa([FromBody] p p1)
{
                try {
...
return Ok(xxx);
                }
                catch (Exception ex)
                {
                    return BadRequest(ex.Message);
                }

產生token
var secret = System.Web.Configuration.WebConfigurationManager.AppSettings["JWTSecret"];
var token = JWT.Builder.JwtBuilder.Create()
  .WithAlgorithm(new JWT.Algorithms.HMACSHA256Algorithm()) // symmetric
  .WithSecret(secret)
  .AddClaim("exp", DateTimeOffset.UtcNow.AddMinutes(1).ToUnixTimeSeconds())
                  .AddClaim("role", 123)
  .Encode();

呼叫端
using (HttpClient httpClient = new HttpClient())
{
httpClient.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", token);
httpClient.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json"));
var stringContent = new StringContent(JsonConvert.SerializeObject(new { lineid = "12345" }), System.Text.Encoding.UTF8, "application/json");
using (var response = await httpClient.PostAsync("...", stringContent))
{
string responseBody = await response.Content.ReadAsStringAsync();
var jobj = JsonConvert.DeserializeObject<JObject>(responseBody);
}
}

asp.net core mvc api 學習心得 (使用JWT驗證權限)

被呼叫端
startup.cs
public void ConfigureServices(IServiceCollection services, IConfiguration configuration)
{
services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
.AddJwtBearer(options =>
{
options.IncludeErrorDetails = true; 
options.TokenValidationParameters = new TokenValidationParameters { 
        // 透過這項宣告,就可以從 "sub" 取值並設定給 User.Identity.Name
        NameClaimType = "http://schemas.xmlsoap.org/ws/2005/05/identity/claims/nameidentifier",
        // 透過這項宣告,就可以從 "roles" 取值,並可讓 [Authorize] 判斷角色      
        RoleClaimType = "http://schemas.microsoft.com/ws/2008/06/identity/claims/role",  
        ValidateIssuer = true, 
ValidIssuer = Configuration.GetValue<string>("JwtSettings:Issuer"),
ValidateAudience = false, 
ValidateLifetime = true, 
        ValidateIssuerSigningKey = true, 
IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(Configuration.GetValue<string>("JwtSettings:SignKey"))) 
};
});

public void Configure(IApplicationBuilder app)
{
app.UseRouting()
app.UseAuthentication();
app.UseAuthorization();
app.UseEndpoints(...);

[Authorize(Roles="xxx,ooo")] // 指定可以存取的角色
[ApiController]    
[Route("api/[controller]")]
public class xxxAPIController : Controller
{        
        [HttpGet("[action]")]
        public async Task<IActionResult> 查詢(int courseid, bool? 尚未確認, bool? 推薦人聯絡未完成, int? sid) // 接收多個簡單變數(字串有預設長度限制,超過會出現 http 404)
        {

public class 未出貨訂單參數
{
public string lineid { get; set; }
}
[HttpPost("[action]")]
public async Task<IActionResult> 未出貨訂單(未出貨訂單參數 p) // 預先定義class 接收物件參數
{           

        [HttpPost("[action]")]
        public async Task<IActionResult> 查詢推薦人聯絡(Dictionary<string, dynamic> p) // 針對多個簡單變數及陣列直接轉換參數
        {
            var ids = (p["ids"] as Newtonsoft.Json.Linq.JArray).ToObject<List<int>>();
            var id = int.Parse(p["id"] as string);
}


呼叫端 (C#)
using (HttpClient httpClient = new HttpClient())
{
        //產生token
        var claims = new List<Claim>() {
                new Claim (JwtRegisteredClaimNames.NameId, 登入帳號),
            };
        roles.ForEach(a => claims.Add(new Claim(ClaimTypes.Role, a.ToString()))); // 加入角色以供被呼叫端驗證是否有權限
        var tokenG = new JwtSecurityToken(
                issuer: validIssuer,
                audience: validAudience,
                claims: claims,
                expires: DateTime.UtcNow.AddDays(1), // 一天後過期
                signingCredentials: new SigningCredentials(new SymmetricSecurityKey(Encoding.UTF8.GetBytes(issuerSigningKey)),
                    SecurityAlgorithms.HmacSha256)
            );
        var tokenHandler = new JwtSecurityTokenHandler();
        string token = new JwtSecurityTokenHandler().WriteToken(tokenG);

httpClient.DefaultRequestHeaders.Authorization =  new AuthenticationHeaderValue("Bearer", token);
httpClient.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json"));
var stringContent = new StringContent(JsonConvert.SerializeObject(new { lineid= "12345" }), System.Text.Encoding.UTF8, "application/json");
using (var response = await httpClient.PostAsync("...", stringContent))
{
string responseBody = await response.Content.ReadAsStringAsync();
var jobj = JsonConvert.DeserializeObject<JObject>(responseBody);
}
}


呼叫端 (axios)
將取得的 jwt token 存放在 cookie,每次呼叫時帶入
axios.interceptors.request.use((config: AxiosRequestConfig) => {
    const token = Cookies.get('...');
    if (config.headers) {
        config.headers.Authorization = `Bearer ${token}`;
    }
    return config;
})
axios.get('...', { params: { id:1 } }) // get帶入簡單變數
axios.post('...', null, { params: { id:1 } }) // post帶入簡單變數 (參數值有字數限制,超過會出現 "Request failed with status code 404")
axios.post('...', {id:1,ids:[1,2,3]}) // post帶入物件


※ 設定發布時同步上傳的資料夾及檔案
<ItemGroup>
  <Content Remove="$(SpaRoot)**" />
  <Content Include="資料夾\*" CopyToOutputDirectory="PreserveNewest" /> // 建置動作設為"內容"
</ItemGroup>

2021年12月8日 星期三

asp.net 網站無法同時處理多個請求

此為 session block 造成的,當 iis 執行一個請求時,若用到session,會把session lock,下一個請求會等待unlock 後才能存取session,之後才會執行後續程式

請關閉session,若有用到session 則改用其他方式做資料交換,例如用memorycache 記錄使用者權限

web.config
<system.web>
<sessionState mode="Off" />

public class 權限設定 {
class 使用者權限
{
public int userid { get; set; }
public string functionid { get; set; }
}
static ObjectCache cache = MemoryCache.Default;
static List<使用者權限> 使用者權限cache
{
get
{
if (cache["使用者權限"] == null) cache["使用者權限"] = new List<使用者權限>();
return cache["使用者權限"] as List<使用者權限>;
}
set
{
CacheItemPolicy policy = new CacheItemPolicy();
policy.AbsoluteExpiration = DateTimeOffset.Now.AddDays(1);
cache.Add("使用者權限", value, policy);
}
}
public static bool 檢查權限(string ID, System.Web.UI.Page page)
{
var userid = page.User.Identity.Name.ToInt();
if (!使用者權限cache.Any(a=>a.userid==userid))
{
using (var db = new xxxEntities())
{
使用者權限cache.AddRange(db.xxx.Where(a=>a.UserID==userid).Select(a =>new 使用者權限() { userid = a.UserID, functionid = a.FunctionID }).ToList());
}
}
return 使用者權限cache.Any(a => a.userid == userid && a.functionid == ID);
}
public static void 清除使用者權限cache(Page page)
{
var userid = page.User.Identity.Name.ToInt();
使用者權限cache.RemoveAll(a => a.userid == userid);
}
}

2021年12月6日 星期一

asp.net webmethod async 寫法

 c#
====
[WebMethod]
public async static Task<string> 查詢Async()
{
using (var db = new Entities1())
{
var xxx =(await db.xxx.Where(a => ...).ToListAsync().ConfigureAwait(false)).Select(a => ...);
※加入 .ConfigureAwait(false) 避免無窮等待,原因可參考這篇,簡短說明參考:
To avoid performance degradation and possible dead-locks in ASP.NET or WPF applications (or any SynchronizationContext-dependent environment), you should always put ConfigureAwait(false) in your await statements:

javascript
====
$http.post('xxx.aspx/查詢Async', { })
.then(function (result) {
var newlist = JSON.parse(result.data.d.Result);
※回傳資料會放在Result裡面

aspx
====
<%@ Page Async="true" 

2021年12月3日 星期五

呼叫非同步方法不等待且visual studio 不要警告

安裝套件 Microsoft.VisualStudio.Threading

非同步方法最後加入.Forget()
ex. xxx.xxxAsync(...).Forget();

2021年11月17日 星期三

使用 await 等待非同步函數問題

MVC
====
若controller 有全域 DbContext 物件,執行 await 後會造成 物件自動dispose
解決方式為不使用全域DbContext 物件,需要時再透過 using 建立


webforms
====
webmethod 執行 await 後就無法再取得 System.Web.HttpContext.Current,必須先用變數記錄起來,await 之後再填回
var httpcontext = HttpContext.Current;
await xxxAsync().ConfigureAwait(false);
HttpContext.Current = httpcontext;

2021年11月5日 星期五

b-modal 實作範例

顯示表格資料,頂部有欄位名稱,底部有按鈕列,中間有捲軸
<b-modal scrollable size="xl" id="b-modal-列印託運單" no-close-on-backdrop="true" hide-header-close="true">
            <template #modal-header>
                <b-row style="width:100%">
                    <b-col class="col-2">訂單編號</b-col>
                    <b-col class="col-2">客戶名稱</b-col>
                    <b-col class="col-6">品名</b-col>
                    <b-col class="col-2">商品類別</b-col>
                </b-row>
            </template>
            <div class="d-flex align-items-center" v-for="需列印託運單 in 需列印託運單s" style="padding:0.25em;margin-bottom:0.5em">
                <b-row style="width:100%">
                    <b-col class="col-2">{{需列印託運單.訂單號}}</b-col>
                    <b-col class="col-2">{{需列印託運單.客戶名稱}}</b-col>
                    <b-col class="col-6">{{需列印託運單.品名}}</b-col>
                    <b-col class="col-2">
                        <b-form-select v-model="需列印託運單.商品類別" :options="商品類別s" style=" width: 100%; display: inline-block " value-field="value" text-field="text">
                        </b-form-select>
                    </b-col>
                </b-row>
            </div>
            <template #modal-footer>
                <b-button-group>
                    <b-button variant="light" v-on:click="click取消列印託運單()">取消</b-button>
                    <b-button variant="primary" style="width:6em" v-on:click="click開始列印託運單()">列印</b-button>
                </b-button-group>
            </template>
</b-modal> 

區塊陰影效果

 <div style="margin: 0.5em; box-shadow: 0.25em 0.25em 0.5em #cccccc;"></div>

2021年10月26日 星期二

使用異步延遲進行同步等待

await Task.Delay(1000);

※避免使用 Thread.Sleep 以免阻塞執行緒,且在 windows 表單程式造成 UI 無回應


2021年10月18日 星期一

使用 SignalR 讓 server 主動推播資料給 client (Asp.Net Core)

c#
1. startup.cs 加入signalr
public void ConfigureServices(IServiceCollection services)
{
    ...
    services.AddSignalR();
    services.AddCors(options =>
                options.AddDefaultPolicy(builder => builder.WithOrigins("https://localhost:44331").AllowAnyHeader().AllowAnyMethod().AllowCredentials())); // 針對跨網域呼叫加入來源白名單
}
public void Configure(IApplicationBuilder application)
{
    application.UseRouting();
    application.UseCors(); // 處理跨網域呼叫
    application.UseEndpoints(endpoints =>
    {
        endpoints.MapHub<ChatHub>("/hub");
    });
    ....
}

2. 建立hub class
using Microsoft.AspNetCore.SignalR;
    public class ChatHub : Hub
    {
        // 接收通知
        public Task ClientMessage(string name,string message)
        {
           ...
        }
    }


3. 發送通知
private readonly Microsoft.AspNetCore.SignalR.IHubContext<ChatHub> _hubContext;
public xxxController(Microsoft.AspNetCore.SignalR.IHubContext<ChatHub> hubContext) {
    _hubContext = hubContext;
}
public async Task<IActionResult> xxx() {
   await _hubContext.Clients.All.SendCoreAsync("訂單異動", new object?[] { 訂單編號});
}

----

js
1. 安裝 signalr : npm install @microsoft/signalr
2. 引入 node_modules\@microsoft\signalr\dist\browser\signalr.min.js
3. 設定SignalR的連線資訊
var connection = new signalR.HubConnectionBuilder().withUrl("https://xxx/hub").withAutomaticReconnect().build();
4. 與Server建立連線
async function start() {
    try {
        await connection.start({ transport: 'webSockets' });
        console.log("SignalR Connected.");
    } catch (err) {
        console.log(err);
        setTimeout(start, 5000);
    }
};
// Start the connection.
start();
5. 接收通知
connection.on("訂單成立", function (message) {
    console.log(message);
});
6. 發送通知
connection.invoke("ClientMessage", 'ReceiveMessage', 'hihihi').catch(function (err) {
    return console.error(err.toString());
});

2021年10月13日 星期三

focus 用法注意

若搭配vue 等前端框架動態產生dom,有時會有時間差,必須確定已產生dom再呼叫focus
ex:
var interval = setInterval(function () {
                                let elem = $('div[name=div組合商品明細').first().find('input').first();
                                if (elem) {
                                    $(elem).focus();
                                    clearInterval(interval);
                                }
                            }, 100);  

2021年10月8日 星期五

asp.net core 應用程式 SSL 設定

1.開發環境啟用 SSL



2.發佈設定調整
修改 .pubxml 加入
<AllowUntrustedCertificate>True</AllowUntrustedCertificate>

※若同時有非 core 專案,必須一併改為啟用SSL,否則會無法啟動偵錯,因為 IIS 啟用SSL 後無法同時處理 http



2021年10月4日 星期一

BootstrapVue 利用 b-row 實作響應式設計


例如若為筆電或桌機則用一列顯示兩個選單,若為手機則一列顯示一個選單
<b-row>
  <b-col class="col-12 col-lg-6">
                    尺寸:<b-form-select v-model="規格" :options="規格s" style=" width: 8em; display: inline-block " value-field="value" text-field="text">
                    </b-form-select>
  </b-col>
  <b-col class="col-12 col-lg-6" style="display: flex; align-items: center;">
                    品名:<b-form-select v-model="商品類別" :options="商品類別s" style=" width: 10em; display: inline-block " value-field="value" text-field="text">
                    </b-form-select>
  </b-col>
</b-row>

※b-col 中若同時包含文字跟input、select,則要加上 style="display: flex; align-items: center;" 會比較美觀,否則文字會偏上沒有置中對齊很醜!

2021年9月17日 星期五

b-table 實作選擇機制

html
<b-table borderless="true" head-variant="dark" fixed sticky-header striped hover :items="銷貨單明細" :fields="欄位s" :filter="filter" selectable select-mode="single" ref="table銷貨單明細" v-on:row-selected="onRowSelected">
<template #cell(已選擇)="data">
{{data.rowSelected?'&#10146;':''}} // 標示選擇列
</template>
</b-table> 

javascript
data() {
        return {
選擇位置: null,
已選擇明細s: [],
欄位s: [
{
key: '已選擇',
label: '',
thStyle: { width: '1em' },
tdClass: 'text-center',
},            
...
computed: {
        已選擇明細() {
            for (let a of this.銷貨單明細) {
                if (a.項次 == this.已選擇明細s[0].項次) {
                    return a;
                }
            }            
        },
...
methods: {
        onRowSelected(items) {
            this.已選擇明細s = items;
            if (items.length > 0) this.選擇位置 = this.銷貨單明細.indexOf(this.已選擇明細);
            else if (this.選擇位置 != null) this.選擇明細();
        },
        選擇明細() {
            let $vue = this;
            if (!$vue.$refs.table銷貨單明細.isRowSelected($vue.選擇位置))
                setTimeout(function () {
                    $vue.$refs.table銷貨單明細.selectRow($vue.選擇位置);
                    $vue.選擇明細();
                }, 100);
        },
...
watch: {
       銷貨單明細() {
            if (this.銷貨單明細.length > 0) {
                if (this.選擇位置!=null) this.選擇明細();
            }
        },
...

※透過onRowSelected得到的物件必須視為唯讀,和實際物件不是同一個記憶體位置,而是複製出來的,不可直接判斷物件是否相等,若要修改則先找出對應的物件再修改,可透過computed回傳對應物件

利用 javascript 複製物件

產生新物件(深層複製)
JSON.parse(JSON.stringify(xxx))

覆蓋屬性值(淺層複製)
Object.assign(target,source)

2021年9月16日 星期四

BootstrapVue 學習心得

驗證 form 元素範例
<BFormInput :id="input-name" placeholder="請選擇" v-model="name" :state="!name? false:null"></BFormInput>
<BFormInvalidFeedback :id="input-name-feedback">請輸入姓名</BFormInvalidFeedback>


下拉選單分群
選項資料用 label 搭配 options 餵給 b-form-select,label 就是群組名稱
ex.
後端
活動s.Where(a => a.時間 >= DateTime.Today.FirstDayOfMonth()).OrderBy(a => a.時間).ToList().GroupBy(a=>a.時間.FirstDayOfMonth()).Select(a=>new { label=a.Key.ToString("yyyy/M"), 
options=a.Select(b => new { value = b.id, text = b.content }).ToList() })
前端
<b-form-select v-model="活動" :options="活動s"></b-form-select>


讓 bootstrapvue toast 顯示 html or 替換內容
javascript
this.提醒訊息 ='aaa<br>bbb';
this.$bvToast.show('toast提醒訊息');

html
<b-toast id="toast提醒訊息" title="提醒訊息" variant="warning" solid no-auto-hide>
        <div v-html='提醒訊息'></div>
</b-toast>

2021年9月13日 星期一

b-table 學習心得

※table 綁定的 items 和原始陣列不同,是透過複製得到,因此修改 items 不會連動修改原始陣列

事件函數不支援中文,除非加上(),但這樣就無法取得預設參數
ex: v-on:row-clicked="選擇資料"

垂直置中
.table td, .table th {
        vertical-align: middle;
}
內距縮小 : 加入 small 屬性

若內含按鈕會呼叫後端,搭配 b-spinner 會造成目前捲軸位置跑掉
解法1 : 記憶捲軸位置
解法2 : 呼叫後端不要顯示 b-spinner (其他情況呼叫後端才顯示 b-spinner)
※ 可能是 webform 才會有此問題
※ 後來發現使用 b-pagination 分頁時,change 事件會在未換頁時一樣觸發(例如新增或刪除資料),若於換頁時自動設定捲軸置頂,就會被干擾,必須另外用變數記錄頁數比對是否真的有換頁

垂直捲軸置頂(通常用在查詢後)
$('#b-table').parent().scrollTop(0);

加入 primary-key 屬性可以改善效能(尚須確認)
<b-table primary-key="xxx" 

過濾文字機制不要直接綁定屬性,改用 watch 延遲設定,避免資料較多(超過100筆)時輸入文字卡卡
<b-input v-model="filtertext" placeholder="過濾..."></b-input> 
<b-table :filter="filter"
 watch: {
                filtertext(newVal, oldVal) {
                    clearTimeout(this.$_timeout);
                    this.$_timeout = setTimeout(() => {
                        this.filter = newVal
                    }, 500); // set this value to your preferred debounce timeout
                }
            },

window.innerHeight vs $(window).height()

原則上兩個回傳的值一樣,但
1. $(xxx).height() 不會考慮開啟開發人員工具的情況
2. 若用在透過 $(xxx).height() 設定容器高度,某些用法會有不同結果,window.innerHeight 的結果才是正確的

結論: 一律使用 window.innerHeight 取得視窗高度

2021年8月18日 星期三

action 加入 async 造成前端無法呼叫

c#
====
public async void aaa()
{
}

javascript
====
axios.post('./aaa')
.then(response => {                               
}).catch(function (error) {
console.log(error);
})

解法:
1. void 改成 Task
2. web.config 加入
<add key="aspnet:AllowAsyncDuringSyncStages" value="true"/>
※理論上不該允許同步中包含非同步,此為寫法錯誤

javascript 透過 async 和 await 進行同步作業

async aaa() {
        let result=0;
await axios.post('./aaa')
.then(response => {
                        result=response.data;
}).catch(function (error) {
console.log(error);
});     
        consloe.log(result); // 等待 post 完成後才會執行
}

2021年8月17日 星期二

呼叫網址及驗證機制

呼叫端
using (HttpClient httpClient = new HttpClient())
{
httpClient.DefaultRequestHeaders.TryAddWithoutValidation("Authorization", token);
var stringContent = new StringContent(JsonConvert.SerializeObject(new {p1=123,p2="aaa"}), System.Text.Encoding.UTF8, "application/json");
using (var response = await httpClient.PostAsync(url, stringContent)) 
{ }
}

被呼叫端
[AllowAnonymous]
[HttpPost]
public void xxx(int p1,string p2)
{
if (Request.Headers["Authorization"]!=token) {
                Response.Write("非法呼叫");
                Response.StatusCode = (int)System.Net.HttpStatusCode.Unauthorized;
                return;
        }
}

2021年7月28日 星期三

placeholder css

ex: 設定輸入方塊文字大小,連同提示文字
input,input::-webkit-input-placeholder {
 font-size:2em;
}

2021年6月28日 星期一

判斷char 是否為UTF16

bool isUTF16(char value)
            {
                return Convert.ToInt32(value) >= Convert.ToInt32("d800", 16) && Convert.ToInt32(value) <= Convert.ToInt32("dfff", 16);
            }

※中文字串透過 ToArray() 拆解成 char 陣列時,特殊字會用UTF16處理,一個中文字會變成兩個char(顯示為\uxxxx),並使用d800~dfff的code point

2021年6月22日 星期二

windows form AutoScaleMode 和 windows 縮放比例的關係

inherit,none : 一起放大縮小,可能會造成視窗某些區域超出螢幕之外

dpi,font : 一起放大縮小,但視窗高度比例較小造成內部元件可能會超過底部

執行檔右鍵 > 內容 > 相容性 > 變更高DPI設定 改成下面設定 : 只有字型一起放大縮小,基本上不太會有問題,是目前最好的解法



2021年6月15日 星期二

連線字串動態設定密碼

建立 xxxEntities 的擴充class,加入方法 : CreateDbContext
public partial class xxxEntities : DbContext
    {
        private xxxEntities(string connectionString)
        : base(connectionString)
        {
        }

        public static xxxEntities CreateDbContext()
        {
            var efbuilder = new EntityConnectionStringBuilder(System.Configuration.ConfigurationManager.ConnectionStrings["xxxEntities"].ConnectionString);
            var sqlbuilder = new SqlConnectionStringBuilder(efbuilder.ProviderConnectionString);
            sqlbuilder.Password = "xxx";
            efbuilder.ProviderConnectionString = sqlbuilder.ConnectionString;
            return new xxxEntities(efbuilder.ConnectionString);
        }
    }

使用方式
using (var db=xxxEntities.CreateDbContext())

2021年6月9日 星期三

判斷檔案是否鎖定中

例如正在寫入中,檔案還不完整

public static bool IsFileLocked(this System.IO.FileInfo file)
    {
        System.IO.FileStream stream = null;
        try
        {
            stream = file.Open(System.IO.FileMode.Open, System.IO.FileAccess.Read, System.IO.FileShare.None);
        }
        catch 
        {
            //the file is unavailable because it is:
            //still being written to
            //or being processed by another thread
            //or does not exist (has already been processed)
            return true;
        }
        finally
        {
            if (stream != null)
                stream.Close();
        }
        //file is not locked
        return false;
    }

2021年6月8日 星期二

使用 jquery 操作 radio list

<label><input type="radio" name="xxx" value="xxx" />xxx</label>
<label><input type="radio" name="xxx" value="xxx" />xxx</label>


取得選取值
$("input:radio[name='xxx']:checked").val()

設定選取值
$(`input:radio[name='xxx'][value='${xxx}']`).prop('checked', true);

清空選取值
$("input:radio[name='xxx']").prop('checked',false);

2021年6月3日 星期四

判斷兩個區間是否重疊

假設有兩個區間 a,b
分別有start 和end
重疊會有四種情況
1: a.start<=b.start and a.end>b.start
2: b.end>a.start and b.end<=a.end
3: a.start<=b.start and a.end>=b.end
4: b.start<=a.start and b.end>=a.end
簡易判斷法 : max(a.start,b.start)<min(a.end,b.end)

2021年4月14日 星期三

b-table 分頁設定

<b-pagination style="margin:1em 0" v-model="currentPage" :total-rows="totalRows" :per-page="perPage" :change="換頁()"></b-pagination>

<b-table id="b-table" :per-page="perPage" :current-page="currentPage" head-variant="dark" v-grid fixed sticky-header striped hover :items="gridData" :fields="fields" :filter="filter" v-on:filtered="onFiltered">  

data() {
                return {
                    perPage: 100,
                    currentPage: 1,
                    totalRows:0, // 取得資料後更新

methods: {
                換頁() {
                     // 每次換頁時自動捲到最上面(若有異動資料也會觸發,必須另外用變數記錄目前是第幾頁來決定是否置頂)
                     if (this.currentPage != this.prePage) {
                        $('#b-table').parent().scrollTop(0);
                        this.prePage = this.currentPage;
                    }
                },
                onFiltered(filteredItems) { // 為了filter 可以跨頁正確運作
                    this.totalRows = filteredItems.length; 
                    this.currentPage = 1;
                },

2021年4月12日 星期一

設定元素高度為可用剩餘高度

<html style="height: 100%;">
<body style="height: 100%; display: flex; flex-direction: column;">
<div>...</div>
<div id="app" style="flex-grow: 1; flex-basis: 0; display: flex; flex-direction: column;">
  <div>...</div>
  <form id="form1" style="flex-grow: 1; flex-basis: 0; display: flex; flex-direction: column;">
      <b-table style="flex-grow: 1; flex-basis: 0; max-height:unset " sticky-header :items="items" :fields="fields" :filter="filter">
      </b-table>
  </form>
</div>
</body>
</html>

※設定 max-height:unset 覆蓋原預設值:300px

b-table sticky column 針對多個欄位凍結的設定方式

第二個以後的欄位需要設定left 避免往右捲動時覆蓋前面的欄位

<style>
        ._2ndtd {
            left:50px !important;
        }
        ._3ndtd {
            left:140px !important;
        }
</style> 

fields: [
                        {
                            key: 'a',
                            stickyColumn: true,
                            thStyle: { width: '50px' },
                        },
                        {
                            key: 'b',
                            stickyColumn: true,
                            thStyle: { width: '90px' },
                            thClass: '_2ndtd',
                            tdClass: '_2ndtd',
                        },
                        {
                            key: 'c',
                            stickyColumn: true,
                            thStyle: { width: '90px'},
                            thClass: '_3ndtd',
                            tdClass: '_3ndtd',
                        },

※ 使用 stickyColumn: true 就不能使用 sortable: true,否則會造成異常現象

2021年3月19日 星期五

ECMAScript 6 學習筆記

Template Literals 字串樣版
====
let name='弘弘';
alert(`我叫${name},你好嗎?` );

Arrow Functions 箭頭函數
====
setInterval(() => console.log(aa),1000); // 無參數,單一指令
.then(result=>{ // 有參數,多個指令
console.log('looped');        
alert('looped');
});

走訪陣列
====
let arr = [1, 2, 3];
for (let a of arr) {
    console.log(a);
}

2021年3月17日 星期三

zoom 學習筆記

預設姓名 (免除進入會議時還要手動輸入姓名)
會議連結帶入參數 : &uname=姜大弘%20(弘弘)

LINE強制用外部瀏覽器開啟會議連結 (iphone尚未安裝zoom app 才能正確跳轉到下載頁面)
會議連結帶入參數 : &openExternalBrowser=1

事件處理 : 進入會議室自動呼叫網址
一、設定 webhook
1. 登入 app maketplace https://marketplace.zoom.us/
2. 下拉選單選擇 build app
3. Webhook Only > create
4. Feature > Event Subscriptions > 新增
Event notification endpoint URL : 輸入要呼叫的網址
Event types : 新增 meeting > Participant/Host joined meeting
二、呼叫網址端程式寫法
if (Request.Headers["authorization"] != [對應 feature 的 Verification Token]) return; // 確認來源合法
      string documentContents = ""; // request body
      using (Stream receiveStream = Request.InputStream)
        using (StreamReader readStream = new StreamReader(receiveStream, System.Text.Encoding.UTF8))
          documentContents = readStream.ReadToEnd();
      var json = JsonConvert.DeserializeObject<Newtonsoft.Json.Linq.JObject>(documentContents);
      var name = json.Value<Newtonsoft.Json.Linq.JObject>("payload").Value<Newtonsoft.Json.Linq.JObject>("object").Value<Newtonsoft.Json.Linq.JObject>("participant").Value<string>("user_name"); // 進入會議室的姓名
      var 會議id = json.Value<Newtonsoft.Json.Linq.JObject>("payload").Value<Newtonsoft.Json.Linq.JObject>("object").Value<string>("id");


2021年3月12日 星期五

清單效果

<dl class="row">
    <dd class="d-flex justify-content-end col-4 col-sm-5 col-md-6">活動:</dd>
    <dd class="col-8 col-sm-7 col-md-6">大溪一日遊</dd>
    <dd class="d-flex justify-content-end col-4 col-sm-5 col-md-6">日期:</dd>
    <dd class="col-8 col-sm-7 col-md-6">2021-12-31</dd>
</dl> 
<style>
  dl dd:nth-child(odd) { // 左邊文字強制不換列,超過則裁切
    padding-left: 0;
    overflow: hidden;
    white-space: nowrap;
    text-overflow:clip;
  }
  dl dd:nth-child(even) { // 右邊文字強制不換列,超過則出現...
    padding-right: 0;
    overflow: hidden;
    white-space: nowrap;
    text-overflow: ellipsis;
  }
</style>

2021年3月10日 星期三

Vue2 學習筆記

引入相關 .js 和 .css

搭配 asp.net mvc razor page 架構,直接引入相關 .js 和 .css,不使用webpack import 方式
<link type="text/css" rel="stylesheet" href="//unpkg.com/bootstrap/dist/css/bootstrap.min.css" />
<link type="text/css" rel="stylesheet" href="//unpkg.com/bootstrap-vue@latest/dist/bootstrap-vue.min.css" />
<script src="//polyfill.io/v3/polyfill.min.js?features=es2015%2CIntersectionObserver" crossorigin="anonymous"></script>
<script src="//unpkg.com/vue@latest/dist/vue.min.js"></script> @*目前版本是2.x*@
<script src="//unpkg.com/bootstrap-vue@latest/dist/bootstrap-vue.min.js"></script>
<script src="//unpkg.com/bootstrap-vue@latest/dist/bootstrap-vue-icons.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/axios/dist/axios.min.js"></script> @*http 請求*@
<script src="~/Scripts/moment-with-locales.min.js"></script> @*date 格式化*@

html

定義app範圍
<div id="app"> 
...
</div>

顯示data
{{plaintext}} 

顯示html文字
<span v-html="htmltext"></span>

input綁定屬性值,.number 表示限制只能輸入數字
<input v-model.number="unitprice" type="number" /> 

.trim 表示自動刪除前後空白字元
<input type="text" v-model.trim="trimMsg" /> 

可綁定陣列屬性
<label><input type="checkbox" value="Jack" v-model="checkedNames">Jack</label> 
<label><input type="checkbox" value="John" v-model="checkedNames">John</label>

href 替換變數bbb
<a v-bind:href='`...?a=${bbb}`' target="_blank">{{ccc}}</a> 

select 綁定用法,v-bind:value="option" 透過 v-bind: 讓屬性 綁定變數
<select v-model="selected"> 
        <option disabled value="">请选择</option>
        <option v-for="option in options" v-bind:value="option.value" v-bind:key="option.value"> @*v-bind:key 指定識別物件的屬性*@
            {{ option.text }}
        </option>
</select>

select 複選綁定用法,搭配ctrl or shift 選擇多筆
<select multiple v-model="multiselected"> 
        <option disabled value="">请选择</option>
        <option v-for="option in options" v-bind:value="option.value">
            {{ option.text }}
        </option>
</select>

v-bind:xxx 對應 attribute:xxx 綁定屬性值,v-on:xxx 對應事件,.stop 表示事件觸發不往上層拋,.prevent 表示不觸發預設行為
<button v-bind:disabled="loading" v-bind:class="{ 'error': 按鈕禁用 }" v-bind:style="按鈕樣式" v-on:click.stop.prevent="下單($event)">下單</button> 

v-on:keyup.enter 表示按enter 後執行
<input type="text" placeholder="輸入任意文字後按下 enter 鍵" v-model.trim="trimMsg"
 v-on:keyup.enter="下單()"> 
v-on:keyup.enter.exact 表示若有搭配其他按鍵則不要觸發 (例如 shift+enter)

依據條件控制是否顯示
<span v-show="按鈕禁用">按鈕禁用</span> 
※若搭配 class:d-flex 會造成 v-show 無法正確隱藏,必須改用 v-if

強制更新頁面區塊,每當componentKey 更新時就會更新此div (重新判斷v-show)
<div v-show="new Date()>=時間 && !已預約" v-bind:key="componentKey">很抱歉,活動已開始,無法預約囉</span>

依據屬性值控制顯示忙碌中圖案,例如呼叫後端尚未完成時,內部元素會暫時無法互動,預設位置是外部容器的正中間
<b-overlay :show="loading" opacity="0.8" variant="light" spinner-variant="primary">
...
</b-overlay>
若要保持捲動瀏覽器視窗時位置不會跑掉,外面必須再包一層div
<div style="position: fixed; top: 2em; width: 100%; z-index: 10;"><b-overlay :show="loading" opacity="0.8" variant="light" spinner-variant="primary"></b-overlay></div>
或單獨使用 b-spinner 顯示忙碌中圖案,各按鈕單獨控制是否暫時disable
<div style="position: fixed;top:1em; left: 50%; transform: translateX(-50%)"  v-show="loading"><b-spinner label="Loading..."  variant="primary"></b-spinner></div>

使用 Bootstrap Icon

<b-icon-[Bootstrap Icon name]></b-icon-[Bootstrap Icon name]>
<b-icon-clipboard></b-icon-clipboard> 
針對 arrow-90deg-left 順時針旋轉90度
<b-icon-arrow-90deg-left style="transform: rotate(90deg)"></b-icon-arrow-90deg-left> 

格式化輸出

{{ xxx | dateFormat('HH:mm')}}
{{ xxx | numberFormat(0,false)  }}

javascript (放在 </html> 後面才能正確載入)
====
設定 moment 為繁體中文
moment.locale('zh-tw'); 

加入filter

// 數值格式化
Vue.filter('numberFormat', function (value, 最大小數位, 千分位符號) {// 小數位、千分位符號
        return Intl.NumberFormat('en-US', { maximumFractionDigits: 最大小數位, useGrouping: 千分位符號 }).format(value);
    });
// 日期格式化
Vue.filter('dateFormat', function (value, myFormat) {
    return moment(value).format(myFormat);
});

載入 BootstrapVue 套件,提供多種美化功能
Vue.use(BootstrapVue) 

建立 vue app

const app = new Vue({ 
        el: '#app', // 綁定 html element
        data() { // 定義屬性及預設值,需要先定義才能使用 v-model
            return {
                plaintext: '123',
                htmltext: '<i>123</i>',
                trimMsg: null,
                loading: false, // 控制顯示忙碌中圖案
            }
        },
        methods: { // 定義函數
            取得參數字串() {
                return this.參數字串;
            },
            下單() {
                this.$bvToast.toast('已下單', {variant: 'success',solid: true, noCloseButton:true}); // 顯示訊息視窗,variant 決定顏色,sold:true 表示不要透明處理,noCloseButton:true 搭配不指定 title 才不會顯示 title bar
            },
            getdata() {
                let $vue = this;
                axios.post('./get', { 參數: '@Model' }) // 呼叫後端
                    .then(response => {
                    this.resultdata = response.data;
                    }).catch(function (error) { // 请求失败处理
                        console.log(error);
                        $vue.$bvToast.toast(error.response.data, { variant: 'danger', solid: true, noCloseButton: true }); // catch 中的 this 是window,需要在之前先用變數儲存 vue 物件
                    });
            },
            並行呼叫() {
                axios.all([axios.post('./get', { 參數: '@Model' }), axios.post('./testpost')]) // 同時呼叫多個後端,全部完成後才進行下一個動作時使用此方法
                    .then(axios.spread((response1, response2) => {
                        this.resultdata1 = response1.data;
                        this.resultdata2 = response2.data;
                    }));
            },
        },
        computed: { // 計算型屬性,html使用時不需要加括號
            金額() {
                return this.unitprice * this.number;
            },            
        },
        watch: { // 監控屬性變化
            trimMsg(val, oldValue) { // 當trimMsg更新時觸發
                console.log(`新的 msg: ${val}`);
                console.log(`舊的 msg: ${oldValue}`);
            }
        },
        created: function () { // app 建立後觸發
            let $vue = this; // 預先儲存 app 以供函數使用
            axios.interceptors.request.use( // 呼叫後端前觸發
                function (config) {
                    $vue.loading = true; // 搭配b-spinner顯示忙碌中圖案,且透過 v-bind:disabled 避免重複執行click
                    return config;
                },
                function (error) {
                    $vue.loading = false; // 呼叫失敗時隱藏忙碌中圖案
                    return Promise.reject(error);
                }
            );
            axios.interceptors.response.use(function (response) { // 呼叫後端完成後觸發
                $vue.loading = false; // 呼叫完成後隱藏忙碌中圖案
                return response;
            }, function (error) {
                $vue.loading = false; // 呼叫完成但失敗時隱藏忙碌中圖案
                return Promise.reject(error);
            });
        },
        mounted: function () {
            this.$nextTick(function () {
                // 放置頁面載入後要執行的初始動作,類似 jquery $(document).ready(function() {})
                // 若要立刻呼叫 axios.post 必須先等候一下,否則 b-overlay 無法正確顯示
                // ex. setTimeout(() => this.查詢訂單(), 100);
            })
        },
    });

避免中文字出現亂碼

1. <head></head> 加入 <meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
2. 檔案編碼必須為"具有BOM 的 UTF-8",若無則用記事本另存,編碼選擇 具有BOM 的 UTF-8 (visual studio create view 預設此編碼)

混入 (可放共用函數)

import { commonMixin } from '@Url.Content("~/scripts/commonVue.js")'
Vue.mixin(commonMixin); // 放在 Vue.filter 之後避免 filter 失效
commonVue.js :
export const commonMixin = {
    methods: {       
        onerror: function (error) {
            console.error(error);
            if (error.response.status == 401) {
                alert('登入逾時');
                location.reload();
            }
            else
            this.$bvToast.toast(error.response.data, { variant: 'danger', solid: true, noAutoHide: true });
    },
  }
}

※若要動態新增物件屬性,必須在綁定到 vue 之前完成,否則會喪失DOM雙向綁定機制
const rows = [{value:1},{value:2},];
rows.forEach(a => {
a.text = '...';
});                                        
this.rows = rows;


相關連結

2021年2月20日 星期六

epplus 排序欄位

從第5列第2行到最後一列最後一行排序,先排序第2行再來第3行

ws.Cells[5, 2, ws.Dimension.Rows, ws.Dimension.Columns].Sort(new int[] { 0,1 });

/// <summary>
/// Sort the range by value
/// </summary>
/// <param name="columns">The column(s) to sort by within the range. Zerobased</param>
/// <param name="descending">Descending if true, otherwise Ascending. Default Ascending. Zerobased</param>
/// <param name="culture">The CultureInfo used to compare values. A null value means CurrentCulture</param>
/// <param name="compareOptions">String compare option</param>
public void Sort(int[] columns, bool[] descending=null, CultureInfo culture=null, CompareOptions compareOptions=CompareOptions.None)

2021年2月3日 星期三

使用 liff 取得 lineid

新增 liff app
前往 LINE Developers > Provider > channels > 點選 line login channel > liff

取得 lineid
<script src="https://static.line-scdn.net/liff/edge/2/sdk.js"></script>
liff.init({
                liffId: '...'
            }).then(function () {
                if (liff.isLoggedIn()) {
                    liff.getProfile().then(profile => {
                        alert(profile.userId); // lineid
                    });
                }
                else {
                    liff.login({ redirectUri: new URL(window.location.href) }); // 返回時保留呼叫前的參數
                }
            }).catch(function (error) {
                console.log(error);
            });

2021年1月27日 星期三

動態產生超連結底線

.動態超連結 {
    text-decoration: none;
}
.動態超連結:hover {
    text-decoration: underline;

<a class="動態超連結" href="...">...</a>

2021年1月22日 星期五

網址參數加密

網址: https://aaa.com/controller/?參數1&參數2
加密後參數=Server.UrlEncode(參數.aesEncryptBase64(key))
解密後參數=Request.QueryString[0].aesDecryptBase64(key)
參數1=解密後參數.Split('&')[0]
參數2=解密後參數.Split('&')[1]

※加密使用AES
public static string aesEncryptBase64(this string SourceStr, string CryptoKey)
        {
            string encrypt = "";
            AesCryptoServiceProvider aes = new AesCryptoServiceProvider();
            MD5CryptoServiceProvider md5 = new MD5CryptoServiceProvider();
            SHA256CryptoServiceProvider sha256 = new SHA256CryptoServiceProvider();
            byte[] key = sha256.ComputeHash(Encoding.UTF8.GetBytes(CryptoKey));
            byte[] iv = md5.ComputeHash(Encoding.UTF8.GetBytes(CryptoKey));
            aes.Key = key;
            aes.IV = iv;
            byte[] dataByteArray = Encoding.UTF8.GetBytes(SourceStr);
            using (MemoryStream ms = new MemoryStream())
            using (CryptoStream cs = new CryptoStream(ms, aes.CreateEncryptor(), CryptoStreamMode.Write))
            {
                cs.Write(dataByteArray, 0, dataByteArray.Length);
                cs.FlushFinalBlock();
                encrypt = Convert.ToBase64String(ms.ToArray());
            }
            return encrypt;
        }
        public static string aesDecryptBase64(this string SourceStr, string CryptoKey)
        {
            string decrypt = "";
            AesCryptoServiceProvider aes = new AesCryptoServiceProvider();
            MD5CryptoServiceProvider md5 = new MD5CryptoServiceProvider();
            SHA256CryptoServiceProvider sha256 = new SHA256CryptoServiceProvider();
            byte[] key = sha256.ComputeHash(Encoding.UTF8.GetBytes(CryptoKey));
            byte[] iv = md5.ComputeHash(Encoding.UTF8.GetBytes(CryptoKey));
            aes.Key = key;
            aes.IV = iv;
            byte[] dataByteArray = Convert.FromBase64String(SourceStr);
            using (MemoryStream ms = new MemoryStream())
            {
                using (CryptoStream cs = new CryptoStream(ms, aes.CreateDecryptor(), CryptoStreamMode.Write))
                {
                    cs.Write(dataByteArray, 0, dataByteArray.Length);
                    cs.FlushFinalBlock();
                    decrypt = Encoding.UTF8.GetString(ms.ToArray());
                }
            }
            return decrypt;
        }

2021年1月21日 星期四

讓元素寬度自動設為剩餘可用寬度

<div style="display:flex">
<input type="text" style="width:4em">
<select style="flex-grow:1;flex-basis: 0;"></select> 
</div>

下拉選單寬度會等於div 扣除input 後剩餘寬度 (不可設定 width 否則會失效)

若要讓高度設為剩餘可用高度
<div style="height: 100%; display: flex; flex-direction: column;">
  <div>123</div>
  <div style="flex-grow: 1; flex-basis: 0;">剩餘可用高度</div>
</div>

2021年1月19日 星期二

UI Bootstrap Datepicker 和其他物件垂直置中方法

<table><tr >
<td>日期範圍 </td>
<td><span class="input-group" style="display:inline-block;width:125px;margin-bottom:-5px"><input runat="server" id="fromdate" uib-datepicker-popup="yyyy/MM/dd" ui-mask="9999/99/99" model-view-value="true" is-open="popup1.opened" type='tel' class='form-control' style="width: 90px;" ng-model="fromdate"><span class="input-group-btn"><button type="button" class="btn btn-default" ng-click="popup1.opened=true" title="選擇日期"><i class="far fa-calendar"></i></button></span></span></td>
<td>~</td>
<td> <span class="input-group" style="display:inline-block;width:125px;margin-bottom:-5px"><input runat="server" id="todate" uib-datepicker-popup="yyyy/MM/dd" ui-mask="9999/99/99" model-view-value="true" is-open="popup2.opened" type='tel' class='form-control' style="width: 90px;" ng-model="todate"><span class="input-group-btn"><button type="button" class="btn btn-default" ng-click="popup2.opened=true" title="選擇日期"><i class="far fa-calendar"></i></button></span></span></td>
<td> <button class="btn btn-default">查詢</button></td>
</tr></table>
※透過 table td 區隔各元素達到垂直置中
※針對 input-group 設定 margin-bottom:-5px 避免垂直偏差
※針對 input-group 設定 display:inline-block;width:125px 避免轉折

Entity Framework 建立新物件並儲存後馬上取得關聯資料

使用 CreateProxy 建立物件,不要直接 new var newmodel = _contextXXX.CreateProxy<yyy>(); ... _contextXXX.yyy.Add(newmodel); await _contextXXX.SaveC...