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();
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 (一個欄位上限64KB,超過會造成原本table insert 失敗!!)
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();
if (response.StatusCode == HttpStatusCode.OK)
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>
2021年10月26日 星期二
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. 與Server建立連線 - 對應 server 為 core
安裝 signalr : npm install @microsoft/signalr
import * as signalR from "@microsoft/signalr";
var connection = new signalR.HubConnectionBuilder().withUrl("https://xxx/hub").withAutomaticReconnect().build();
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();
//接收通知
connection.on("訂單成立", function (message) {
console.log(message);
});
//發送通知
connection.invoke("ClientMessage", 'ReceiveMessage', 'hihihi').catch(function (err) {
return console.error(err.toString());
});
2. 與Server建立連線 - 對應 server 不為 core
import 'signalr';
import $ from 'jquery';
const huburl = "https://www.lifeacademy.org/cc/signalr";
const connection = $.hubConnection(huburl);
connection.disconnected(() => {
setTimeout(() => {
connection.start({ transport: 'webSockets' });
console.log("Connection restarted");
}, 5000);
});
var proxy = connection.createHubProxy('GetHub');
proxy.on('訂單成立', function (message) {
console.log(message);
});
connection.start({ transport: 'webSockets' });
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 設定
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?'➢':''}} // 標示選擇列
</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
錯誤訊息.value = 錯誤訊息.replaceAll('\n', '<br>');
顯示錯誤訊息視窗.value = true;
html
<Teleport to="body">
<div class="toast-container position-fixed top-0 end-0">
<BToast v-model="顯示錯誤訊息視窗" variant="danger" title="發生錯誤" auto-hide="false"> <div v-html='錯誤訊息'></div>
</BToast>
</div>
</Teleport>
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
}
},
欄位為單一核取方塊的置中效果
<style scoped>
.form-check {
padding-left: 0.5em;
}
:deep(.form-check) .form-check-input {
margin-left: unset;
float:unset;
}
</style>
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日 星期三
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年6月1日 星期二
2021年5月26日 星期三
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>
下拉選單寬度會等於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 避免轉折
訂閱:
文章 (Atom)
input 連結 datalist 用程式控制彈出選項
範例: nextTick(() => document.querySelector('input').showPicker()); ※僅支援現代瀏覽器
-
1. 設定檔案下載儲存位置為 C:\Users\%username%\AppData\Local\Google\Chrome\User Data\Default\Cache 2. 勾選"下載每個檔案前詢問儲存位置" 3. 針對不要下載的檔案類型於第一...
-
自動設定欄寬 sheet.Cells.AutoFitColumns(3, 20); // 必須設定 min 跟 max 才會正常作用 凍結欄位 sheet.View.FreezePanes(4, 4); 標題列 ws.PrinterSettings.RepeatRo...
-
使用 FreeSpire.XLS ... ep.Save(); using (var workbook = new Workbook()) using (var memStream = new MemoryStream()) { workbook.LoadFromSt...