雖然微軟已經公布了新一代的HttpClient (Windows.Web)並解決了不少既有的問題,但由於專案的整合度與舊架構的相容性等問題,上一個世代的HttpClient (System.Net.Http)仍然還是會用到,本文將介紹System.Net.Http.HttpClient的功能擴充,分別是處理【插入API呼叫(時)與結束時事件處理】與【解決Cookie不共用的問題】。
前言
請注意這是System.Net.Http.HttpClient
開始實作
1.【插入API呼叫(時)與結束時事件處理】,由於程式執行時遇到API讀取狀態時使用者總是會要求要弄出一個"讀取中"的小視窗。身為一個對上下班時間嚴謹的工程師,提出的解法絕對不是在每一段程式碼裡面包個甚麼 await Task.WhenAll之類,因為這樣後續調整程式會搞死自己。因此直接從根本源頭下手,"呼叫時API一併將事件處理放入不就好了?",由此想法出發後一切豁然開朗了起來,就冒出了下面這段東西。
*主要重點為【確保檢查非同步處理只有一個】【實際處理事件應由外部指派(委派/設定)】
public delegate void VoidDelegate();
public partial class HttpClientPlus : HttpClient
{
public HttpClientPlus(VoidDelegate loadingProcessing, VoidDelegate loadingEnd) : base()
{
OnLoadingProcessing = loadingProcessing;
OnLoadingEnd = loadingEnd;
}
public new Task<HttpResponseMessage> SendAsync(HttpRequestMessage request)
{
Task<HttpResponseMessage> responseMsg = base.SendAsync(request);
LoadingProcess(responseMsg);
return responseMsg;
}
public new Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, HttpCompletionOption completionOption, CancellationToken cancellationToken)
{
Task<HttpResponseMessage> responseMsg = base.SendAsync(request, completionOption, cancellationToken);
LoadingProcess(responseMsg);
return responseMsg;
}
}
partial class HttpClientPlus
{
object _Locker { get; }
= new object();
List<Task<HttpResponseMessage>> _LoadingTasks { get; set; }
= new List<Task<HttpResponseMessage>>();
Task _DetectLoadingLoop { get; set; }
= null;
event VoidDelegate OnLoadingProcessing;
event VoidDelegate OnLoadingEnd;
void LoadingProcess(Task<HttpResponseMessage> responseMsg)
{
_LoadingTasks.Add(responseMsg);
lock (_Locker)
{
if (_DetectLoadingLoop == null)
{
_DetectLoadingLoop = CreateDetectLoadingLoop();
_DetectLoadingLoop.ContinueWith(task =>
{
_DetectLoadingLoop = null;
});
}
}
}
Task CreateDetectLoadingLoop()
{
const int msDetectIterval = 300;
return Task.Run(() =>
{
while (_LoadingTasks.Any())
{
OnLoadingProcessing?.Invoke();
Thread.Sleep(msDetectIterval);
_LoadingTasks.RemoveAll(task =>
task.IsCanceled
|| task.IsFaulted
|| task.Status == TaskStatus.RanToCompletion);
}
OnLoadingEnd?.Invoke();
});
}
}
簡單測試
2.【解決Cookie不共用的問題】,每一個 HttpClient 在實體化時,如果沒有特別指派CookieContainer,那個各自的Cookie就會獨立的。因此當多個Api所使用的登入驗證為同一個時(如 Forms Authentication ),如果不小心HttpClient沒有保持單一的話就會毫無懸念的炸掉(因Cookie不共用,自然發出request時不會夾帶授權資訊)。其實這部分就只要稍微改寫一下原本HtppClient的宣告。就可以使其不管怎樣都是共用同一個Cookie。
public partial class HttpClientPlus : HttpClient
{
static HttpClientHandler _Handler { get; }
= new HttpClientHandler { CookieContainer = new CookieContainer() };
public HttpClientPlus(VoidDelegate loadingProcessing, VoidDelegate loadingEnd) : base(_Handler)
{
OnLoadingProcessing = loadingProcessing;
OnLoadingEnd = loadingEnd;
}
}
備註
*請注意這是System.Net.Http.HttpClient
*你說跳出"讀取中的視窗"好像忘了寫?原因是那個也挺複雜的,要抽出成獨立一篇文章介紹。(由於彈跳出的視窗建立執行續不同,也必須不同,因此操作起來有很多需注意的事項)
*根據MSDN(https://docs.microsoft.com/en-us/uwp/api/windows.web.http),Windows.Web.Http.HttpClient只支援UWP app (Nuget上也沒有)
*基本上是為了NSwag寫得沒錯。
延伸閱讀
https://channel9.msdn.com/Events/Build/2013/4-092
備註的備註
*當然後續會把那個讀取事件抽出來成為元件,畢竟實際應用程式需要的資料來源可能不只有單單Api,還可能要與執行的電腦進行其他資料介接(連線)。
public interface ILoadingProcess
{
void AddTask(Task task);
}
public partial class LoadingProcess : ILoadingProcess
{
public LoadingProcess(VoidDelegate loadingProcessing, VoidDelegate loadingEnd)
{
OnLoadingProcessing = loadingProcessing;
OnLoadingEnd = loadingEnd;
}
public void AddTask(Task taskAdd)
{
_LoadingTasks.Add(taskAdd);
lock (_Locker)
{
if (_DetectLoadingLoop == null)
{
_DetectLoadingLoop = CreateDetectLoadingLoop();
_DetectLoadingLoop.ContinueWith(task =>
{
_DetectLoadingLoop = null;
});
}
}
}
}
partial class LoadingProcess
{
object _Locker { get; }
= new object();
List<Task> _LoadingTasks { get; set; }
= new List<Task>();
Task _DetectLoadingLoop { get; set; }
= null;
event VoidDelegate OnLoadingProcessing;
event VoidDelegate OnLoadingEnd;
Task CreateDetectLoadingLoop()
{
const int msDetectIterval = 300;
return Task.Run(() =>
{
while (_LoadingTasks.Any())
{
OnLoadingProcessing?.Invoke();
Task.Delay(msDetectIterval).Wait();
_LoadingTasks.RemoveAll(task =>
task.IsCanceled
|| task.IsFaulted
|| task.Status == TaskStatus.RanToCompletion);
}
OnLoadingEnd?.Invoke();
});
}
}
public partial class HttpClientPlus : HttpClient
{
protected ILoadingProcess LoadingProcess { get; }
public HttpClientPlus(ILoadingProcess loadingProcess) : base()
{
LoadingProcess = loadingProcess;
}
public new Task<HttpResponseMessage> SendAsync(HttpRequestMessage request)
{
Task<HttpResponseMessage> responseMsg = base.SendAsync(request);
LoadingProcess.AddTask(responseMsg);
return responseMsg;
}
public new Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, HttpCompletionOption completionOption, CancellationToken cancellationToken)
{
Task<HttpResponseMessage> responseMsg = base.SendAsync(request, completionOption, cancellationToken);
LoadingProcess.AddTask(responseMsg);
return responseMsg;
}
}
partial class HttpClientPlus
{
static HttpClientHandler _Handler { get; }
= new HttpClientHandler { CookieContainer = new CookieContainer() };
}