Azureを使ってサクッと動画共有サービスを作る
YouTube等の外部Serviceを使用してもいいですが
Privateな環境で動画のアップロードをしたい場合があります
(業務利用ぐらいしか思いつきませんが…)
AzureのMediaService等を使っても良いかもしれませんが
同じくAzureのBlobStorageServiceを使用し、AzureのSDKを利用すれば
手っ取り早くプライベートな環境でビデオのアップロード&視聴が可能となります。
Angular+ASP.netで動画ファイルのアップロードと
視聴ページの作成を行ってみたいと思います
Azureの設定
CORSの設定
CORS(クロスオリジンリソース共有)を設定します。
Storageコンテナにアクセスするため、クロスドメインでアクセスできるように設定を行います。
参考にしたサイトでは、スクリプトで設定の変更を行っていますが
AzureのPortal上からも変更できるようです。
ポータルでCORSの設定を行う
1.リソースグルプでストレージアカウントを選択
2.ストレージアカウントの設定から「CORS」を選択
3.CORSの設定がないと思うので、「追加」を選択
4.上記の参考URLの内容をもとに、CORSの設定を行う
オブジェクトのAPIへのアップロード
前準備
ASP.Netはアップロードファイルの上限がデフォルト4MBとなっているので
受付可能なファイルサイズを拡張して上げる必要があります。
web.config
<system.web> <authentication mode="None" /> <compilation debug="true" targetFramework="4.5.2" /> <!--ファイルサイズの上限を変更しておく --> <httpRuntime targetFramework="4.5.2" maxRequestLength="102400" /> </system.web>
Webページからのファイルのアップロード
WebAPIへのアップロード
inputタグの[type=file accept=‘video/*’]であれば、取得対象が動画ファイルとなります。
(change)でファイル変更時のイベントを補足し、WebAPIにファイルを送る処理を実装します。
今回は横着かまして、Change即WebAPIへって感じの処理ですが
もちろん(change)でオブジェクト退避させ
Submit等のイベントを起点にAPIへおくるのが定石ですね。
<input type='file' accept='video/*' (change)="onChangeInput($event)" />
// Component private onChangeInput(el: any) { // イベントで変更されたValue=ファイルオブジェクトを取得 let file = el.target.files[0]; let formData = new FormData(); formData.append('uploadFile', file, file.name); this._webapi.putUploadFile(formData).subscribe(data => { console.log(data); }); } //---------------------------------------------------------- //webapiに送る処理 putUploadFile(formData: FormData): Observable<any> { return this.postFileData<any>("api/FileOperation", formData); }
WebApiではFormデータを送信する
//httpのPostリクエストでファイルを送信 private postFileData<T>(url: string, sendData: FormData) { return this.http .post(url, sendData, <headersContent>) .map(res => res.json()) .catch(error => { alert(error); return Observable.throw(error) }); }
WebAPIからAzureにBlobデータを登録する
POSTデータからファイルオブジェクトの取得
WebAPIではPOSTされたデータからファイル情報等を抜き出します。
public async Task<HttpResponseMessage> Post() { var response = new HttpResponseMessage(); var httpRequest = HttpContext.Current.Request; var mediaType = Request.Content.Headers.ContentType.MediaType; if (httpRequest.Files.Count > 0) { foreach (string file in httpRequest.Files) { var postedFile = httpRequest.Files[file]; /*→Azureの登録処理へ→*/ } } return response; }
コンテナにオブジェクトの登録
まずはAzureSDKを使用して、登録対象ストレージを選択(なければ作成)します。
var credentials = new StorageCredentials(<ACCOUNT_NAME>, <ACCOUNT_KEY>); var storageAccount = new CloudStorageAccount(credentials, true); var blobClient = storageAccount.CreateCloudBlobClient(); container = blobClient.GetContainerReference(<CONTAINER_NAME>); container.CreateIfNotExists();
AzureのAPIはファイルはByte引数となりますので
httpRequestのFileのStream型をByte型に変換してAzureのAPIに渡します。
[GetBlockBlobReference]でAzureにUpload出来る形にした後
[UploadFromByteArrayAsync]でAzureにアップロードします。
var sendByteData = //file.InputStreamをByte[]変換; //Blobにアップロードする対象のファイルを決定 var blob = container.GetBlockBlobReference(FILE_NAME); //メディアのタイプを決定 blob.Properties.ContentType = mediaType; //Blobにアップロード await blob.UploadFromByteArrayAsync(sendByteData, 0, sendByteData.Length);
SASを付与したURLの生成
BlobへのアクセスはShared Access Signature (SAS) で作成したURLを使用してアクセスします。
その為、作成したデータにアクセスするためのアドレスを生成します。
//コンテナ内のファイルを指定 var blob = container.GetBlockBlobReference(FILE_NAME); //SASアドレスの作成 var sas = blob.GetSharedAccessSignature(new SharedAccessBlobPolicy() { //読取りのみ Permissions = SharedAccessBlobPermissions.Read, //1時間だけアクセス可能 SharedAccessExpiryTime = DateTime.UtcNow.AddHours(1) }); //BlobのURIとマージしてURLを生成 var Url = string.Format("{0}{1}", blob.Uri, sas);
VideoのSrcにバインドしVideoを見れるようにする
JSONでURLを返却したら、VideoのsourceにURLをバインドします。
コストについて
日和って短い時間の動画しか上げていませんが
1週間ほど放置してもTOTALで1円以下ですんでいます。
Webサービスの一環として、かんたんでPrivateな動画共有サービスとしては導入しやすいのかなと思います
jQueyプラグインをAngular v2で使いたい(FullCalendar)
Angularを使用するようになって、jQuery自体は殆ど使用しなくなりました。
Componentベースでアプリを作成したときに
jQueryの処理が全体に波及するので使いにくかったんですね。
(特に同じ部品のComonentを親Componentで何度も使用しているときとか。)
ただ。jQueryの覇権時代が長いだけに
それを使いたいなー。という思いもあるわけです。
なので、今回は、jQueryのプラグインをAngularで使用してみたいと思います。
How to include JQuery plugins in Angular 2 running via webpack
といっても、上記の記事の焼き直しみたいな感じになっていますが…
今回は手っ取り早くangular-cliを使用して、作ってみようと思います。
angular-cliを使用することで
比較的手軽にangularの開発&Build環境を構築することが可能です。
angular-cliの使い方は今回の本筋ではないので下記を参考に。
npmでパッケージをダウンロード
まずは、モノがないと始まらないので
jQueryと今回使用したいFullCalendarをnpmでダウンロードします。
npm install jquery npm install fullcalendar
型定義ファイル+αをダウンロード
jQueryとFullCalendarは型定義ファイルが提供されているので
下記の2つの型定義ファイルをダウンロードします。
npm install @types/jquery npm install @types/fullcalendar
また、FullCalendarの型定義ファイル上で
日付等の指定にmoment.jsの型が指定されているので
moment.jsと、その型定義ファイル @types/momentもダウンロードします。
npm install moment npm install @types/moment
angular-cli.json
Angular-cliを使用している場合
外部コンポーネント等のCSSやJavascriptを使用しやすいです。
angular-cli.jsonに外部JSやCSSを読み込むオプションが存在するためです。
今回、FullCalendarを使用するため、FullCalendarのCSSを別に読み込みます。
"styles": [ "../node_modules/fullcalendar/dist/fullcalendar.css" ]
FullCalendarの実装
参考サイトの通り、FullCalendarを使用するComponentを分離して作成します。
Htmlにng-contentを指定し、そこにjQueryでFullCalendarを注入します。
HTML
<ng-content></ng-content>
Component上のHTMLにはng-contentしかないので
this.elementRef.nativeElementのみで指定できますね。
型定義ファイルを使用することで型検証されるようになるため
どのような値をつっこめば良いのかわかりやすくなります。ビバ型。
TypeScript
this.calendarElement = jQuery(this.elementRef.nativeElement); this.calendarElement.fullCalendar({ editable: true, defaultView: 'agendaWeek', selectable: true, selectHelper: true });
仕上げ
あとは上記で作成したComponentを使用し、[ng serve]で実行して動作確認です。
表示するだけでは味気ないので、下のように書き換えてみました。
this.calendarElement = jQuery(this.elementRef.nativeElement); this.calendarElement.fullCalendar({ allDayText: '終日', editable: true, defaultView: 'agendaWeek', selectable: true, selectHelper: true, slotDuration: moment.duration(15, 'minutes'), slotLabelFormat : 'H:mm', buttonText: { today: '今日' }, monthNames: ['1月', '2月', '3月', '4月', '5月', '6月', '7月', '8月', '9月', '10月', '11月', '12月'], dayNames: ['日曜日', '月曜日', '火曜日', '水曜日', '木曜日', '金曜日', '土曜日'], dayNamesShort: ['日', '月', '火', '水', '木', '金', '土'], views: { month: { columnFormat: 'ddd' }, week: { columnFormat: 'D[(]ddd[)]' }, day: { columnFormat: 'D[(]ddd[)]' } }, select: this.onSelectFunction, eventDragStop: this.onDropFunction, eventResize: this.onReSizeFunction, eventRender: this.addEventRender });
/*FullCalendarで定義されていないイベントを付与する(ダブルクリック)*/ private addEventRender = function(event: FC.EventObject, element: any){ element.bind('dblclick', () => { alert('onDoubleClickEbent'); console.log(event); }); } /*カレンダーセレクト時*/ private onSelectFunction = function(start, end){ let eventTitle = prompt('予定の名称を入力せよ!!'); let eventData: FC.EventObject = { start: start, end: end, title: eventTitle }; this.calendarElement.fullCalendar('renderEvent', eventData, true); this.calendarElement.fullCalendar('unselect'); }.bind(this); /*イベントオブジェクト移動時*/ private onDropFunction = function(event: FC.EventObject){ console.log('Drop!'); console.log(event); }.bind(this); /*イベントオブジェクトサイズ変更時*/ private onReSizeFunction = function(event: FC.EventObject){ console.log('ReSize!'); console.log(event); }.bind(this);
日付の表示Formatを変更し、各イベントの処理を実装します。
イベントの引数はFunctionを指定しますが
その場合、calendarElementのobjectが[this]となります。
Angularを使用するうえで色々と使いづらいので、Functionにthisをbindして
FullCalendarを宣言しているClassのオブジェクトを[this]としています。
そうすることで、Angularの機能の諸々を使用しやすいようにしています。
動作させてみる
Console内部にイベントオブジェクトを表記するようにしています。
moment型のデータ等が問題なく取得できていることが確認できるかと思います。
最後に
一応今回のソースは下記に上げてあります。
Angular2Study/UseFullCalender at master · Takas0522/Angular2Study · GitHub
ASP.net MVC4 + Angular v2 + webpack
以前にASP.Net4+Angularで記事を書いたことがありましたが
当時、R.C版だったこともあるので、現在のVer.で作り直してみようと思います。
といっても前回より環境や情報が整ったこともあり、そんなに難しい内容にはならないと思います。
ただ割と無茶な作り方をしているので、素直にCoreかMVC5を使用することをオススメします。
ASP.net MVC4 + Angular v2について、公式にガイドが出ています。
ただ、EmptyなプロジェクトにAngularを乗っけるものなので簡易気味です。
簡易な状態でとりあえず動かしてみたい!ということであれば
公式のガイド通りにやったほうが良いとおもいます。
前半は、AngularのWebpackのIntroductionの通りに進みます
その後、ASP.netMVC4で使用できるよう調整していきます。
注意
今回のものについては僕がとった方法となるので、絶対条件について記述しているものではありません。
Angularは「~2.4.0」を対象としています。Ver.が変わると、この方法でもエラーが発生することは十二分にありえます。
参考サイト
文中で出てきてますが、今回参考にしたサイトをまとめて掲載します。
Angularのソースを構築する
上記のAngularのWebpackのIntroductionの内容をすべて行います。
記事作成時点だとチュートリアルの内容は古かったようなので
Webpackを使用したAngularのBuild時点でエラーが発生した場合は、下記URLの内容を試してみましょう。
また、下記のようなエラーが発生する場合はnode.jsがv6.9.4以下の状態の可能性があります。
outputOptions.children = options.map(o => o.stats);
そして、VisualStudioのタスクランナーで上記のエラーが発生している場合は下記URLの対策を行います。
チュートリアルで行っている主たるところは下記の内容ですね。
それぞれの内容については今回言及することではないと思うので省きます。
TypeScript開発環境の構築:tsConfigの設定
Webpackビルド設定(Webpack.config)
Unitテストシステムの構築(karima.conf)
チュートリアルを行った上で、Angular単体でビルドがうまくいっているか確認してみましょう。
VisualStudioのBuildでななく、コンソールで「webpack」を実行し確認します。
VisualStudioの画面で「のみ」TypeScriptソースでエラーが出ているときは
AngularのBuild時点でエラーが発生していないが
VisualStudioのエラー一覧でエラーが発生している場合、VisualStudio側の構成に原因があることがあります。
AngularのBuild時はプロジェクトのnode_module内のTypeScriptを参照してくれますが
VisualStudioではnode_moduleの設定を見ずに、VisualStudioが持っているTypeScriptの設定で
エラー判断を行っていることがあるようです。
そんな場合は、下記のそれぞれを行うことで解消できる可能性があります。
TypeSript2.xのダウンロード
使用するTypeSCriptとVisualStudioのTypeScriptが一致していない可能性があります。
VisualStudioのTypeScriptExtensionをインストールしましょう。
Download TypeScript for Visual Studio 2015 - 日本語 from Official Microsoft Download Center
プロジェクトファイルのTypeScriptVerの修正
TypeScriptの旧Verが使用されている環境でプロジェクトを作成した場合
プロジェクトで使用されるTypeScriptVersionがプロジェクト内に残っている場合があります
.cproj内のTypeScriptVersionを変更します。
<TypeScriptToolsVersion>2.x</TypeScriptToolsVersion>
環境変数に残存するTypeSrript情報の削除
VisualStudioの古いTypeScriptバージョン情報が、環境変数に残っている可能性があります。
下記記事の内容をもとに、環境変数の情報を修正しましょう。
ASP.netで使うためのソースの修正
Webpackの修正
現在、バンドルされたファイルは[dist]ディレクトリに格納されています。
VisualStudioの初期配置のパス上には[dist]は存在しないのでちょいと使いづらいです。
[Scripts]ディレクトリに変更しましょう。
webpack.dev.js
output: { - path: helpers.root('dist'), + path: helpers.root('Scripts'), publicPath: 'http://localhost:8080/', filename: '[name].js', chunkFilename: '[id].chunk.js' }
ASP側のRouting設定の変更
クライアントはSPA構成で作成することになるので、ASP側のルーティング等は現状必要ありません。
また、Angularのルーティングで生成されたURLで404を吐かないようにする必要があります。
Web.configで404をが発生した場合、ベースページに飛ばすのも良いかもしれませんが
リダイレクトが発生しまくるので得策ではないかと思われます。
ASPのルーティングとうまく組み合わせて使えば、簡易なLazyLoadingとしても使えるような気がします
(Angular+WebPackでLazyLoadingする機能はありますが…)
ひとまず、ベースとなるページを残して、あとは潰してしまい、それに合わせてRoutingの設置を変更します。
RouteConfig.cs
public static void RegisterRoutes(RouteCollection routes) { routes.IgnoreRoute("{resource}.axd/{*pathInfo}"); - routes.MapRoute( - name: "Default", - url: "{controller}/{action}/{id}", - defaults: new { controller = "Home", action = "Index", id = UrlParameter.Optional } - ); + routes.MapRoute( + name: "Default", + url: "{*url}", + defaults: new { controller = "Home", action = "Index" } + ); }
BundleConfigの変更
バンドルして生成された3種のJavascriptファイルは、BundleConfigでBundleして出力します。
起点をindex.htmlではなくしたので、ComponenntのStylesに関しては、今回の場合は使用し辛いので cs CSSもComponentで取り込まず、外部から直接使用します。
app.component.ts
@Component({ selector: 'my-app', templateUrl: './app.component.html', - styles: ['./app.component.css'] })
なのでCSSもSCSS等でまとめて作っちゃうのがいいかもしれませんね。
今回はチュートリアルで作成したCSSをSCSSで作成し直し、Styles.scssで作り直し
生成されたStyles.cssをバンドルします。
public static void RegisterBundles(BundleCollection bundles) { bundles.Add(new ScriptBundle("~/bundles/src").Include( "~/Scripts/polyfills.js", "~/Scripts/vendor.js", "~/Scripts/app.js" )); bundles.Add(new StyleBundle("~/public/css").Include( "~/public/css/styles.css")); }
_Layout.cshtmlの変更
_Layout.cshtmlの構成をAngularのindex.html同様の構成に変更してきます
<!DOCTYPE html> <html> <head> <base href="/"> <meta charset="utf-8" /> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>@ViewBag.Title - Sample</title> @Styles.Render("~/public/css") </head> <body> <div class="container body-content"> @RenderBody() </div> @Scripts.Render("~/bundles/src") @RenderSection("scripts", required: false) </body> </html>
Home.cshtmlを変更
Angularの起点をHome.cshtmlに作成します。
html
<my-app>NOW LOADING...</my-app>
## App_Start系の変更
App_start等のIdentity等々を潰してまわります。
まぁ残しておいても良いですが。
Angularソースの変更
webconfigを使用した場合、相対ファイルパスで表示するimg等のオブジェクトデータは
Webconfigバンドル時によしなにしてScriptファイルにつっこまれます。
つまり、今回のようにASP.netのcshtmlに乗っける場合は非常に使いにくいです。
例えば下記のようにHTMLを記述してもAngularのBuild時点で存在しないとして怒られて終了します。
<img src = "Script/assets/hogehoge" />
なのでバインド変数を使用します。
app.component.ts
export class AppComponent { private angularPng: string = "public/images/angular.png"; }
app.component.html
<main> <h1>Hello from Angular App with Webpack</h1> <img [src]="angularPng"/> </main>
VisualStudioでデバッグしましょう
通りましたね?
Advanced
root配下に切ったディレクトリにWebAppを配置したばあい
Angularで設定するBASE_HREFを[/]で設定していたら
切ったディレクトリ内に配置していても、AngularのRoutingはRootから生成されます。
そうなった場合、ASP.netのAPIやimage等のObjectの使用に難が生じますので
BASE_HREFを動的に変更させる必要があります。
そしてそのHREFをAngularで取得すれば、API等とのやりとり等に使用できるようになります。
最後に
一応、今回作成したものは、下記に乗せています。
もくもく会でもくもくしたお話
Angular2ばっかりもアレなんで、下記のもくもく会に参加してXamarinばっかり考える時間を設けました。
今回はいうほどはまるポイントがなかったというか
クリティカルな参考サイトが多かったため、参考サイトの紹介程度です。
成果
↓動作結果↓
EnumデータをバインドするPicker
- 参考サイト
ListView表示用のEnumデータコンバータ
- 参考サイト
SQLiteを使用したViewModelベースデータの登録と削除と画面反映
- 参考サイト
他内容で勉強中に行き当たった興味深い参考サイト
AlertViewのカスタマイズ? stackoverflow.com
カスタムレンダラでジェスチャーのイベントを捉える
Angular2のDIで遊ぶ
Angular2のDIについて
ちまちま書いてたんですが度重なるR.C版リリースと
正式版リリースでかなりゴテゴテに回りました。
目指すところ
Angular2のDIをざっくりと理解する
Angular2のDIの書き方を理解する。
DI
一般的にDependency Injection(依存注入)と呼ぼれています。
ASP.netだとUnityで実現されているものですかね?
「 Providerから提供されているインスタンスを特定の変数にInject(注入)する仕組み 」
Angular2においては、上位(または同じ)Componentで
Provideされたインスタンスが注入されるような動作となっています。
//DIされる対象 @Injectable() export class HogeService{ ... } @Component({ selector: "hoge" template: "hogeComponent", provides: [HogeService] }) export class HogeComponent{ constructor( //インスタンスの注入 _hogeService: HogeService ){} ... }
上記のような使い方です。
これで何が嬉しいかというと、コンポーネント間でのデータのやり取りが
疎結合な状態で実現できるということですね。
例えば、上記コンストラクタで「_hogeService」が引数となっていますが
コンポーネント使用時は、これを意識せずに使用することが出来ます
(「出来る」であって意識しなくていいというわけではないです)。
コンポーネント間のDI
provideされたインスタンスは、下位階層のコンポーネントでも使用することが出来るようです。
その際、下位の階層では、provideプロパティを使用する必要はありません。
Angular2のDI構成は下図のようなイメージで可能かと思われます。
上記のCコンポーネントのような
Baseまでに別のコンポーネントを介する下位コンポーネントの場合でも
AコンポーネントでDIしていなくても
Cコンポーネントで上位コンポーネントでProvideされているインスタンスをDI可能なようです。
例ソース
例えば、下図の様な構成のコンポーネント郡を考えてみます。
BaseComponentにChildAComponent/ChildBComponentが存在します。
子コンポーネントはそれぞれ独立したServiceを持っています。
Component郡を包括するServiceはMainServiceのみです。
上記の構成を元に、AとBそれぞれのコンポーネントから
メインに対して文字列をPushする処理を作成してみます。
結果は、下図のようになります。
背景色の異なる領域が、それぞれ別のComponentです。
上図の動作から、別々のコンポーネントが
提供された同一のベースサービスを使用できていることがわかるかと思います。
上図の動作は、下記のようなソースの構成で動作しています。
BaseComponent
Component
@Component({ selector: "base-comp", template: `<h2>PushResult</h2> <div *ngFor="let item of _baseService.baseData"> pushComponent : {{item.pushComponent}} / pushText: {{item.pushText}} </div> <br> <br> <a-comp></a-comp> <b-comp></b-comp>`, providers: [BaseService] }) export class BaseComponent{ constructor( private _baseService: BaseService ){} }
Service
@Injectable() export class BaseService{ baseData: PushFileds[] = []; pushData(component: string, pushText: string){ var pushArray: PushFileds = {pushComponent: component, pushText: pushText}; this.baseData.push(pushArray); } getData(component: string) : PushFileds[]{ return this.baseData.filter((x:PushFileds) => x.pushComponent == component); } } class PushFileds{ pushComponent: string; pushText: string; }
Componentで、今回使用するモジュール全体で使用するBaseServiceをProvideしています。
Component上で使用するため、BaseComponentのConstructorでInjectしています。
BaseServiceは、子ServiceからPushされた値を、加工する機能と
加工した配列のフィルタを行い、結果配列を返却する機能を持っています。
ChildComponent
※A・B双方同様なソースのため、BComponentは割愛します。
Component
@Component({ selector: "a-comp", providers:[ChildAService], templateUrl: "app/acomp/childa.html" }) export class ChildAComponent{ acompText: string; constructor( private _childAService: ChildAService ){} pushText(){ this._childAService.pushText(this.acompText); } }
Service
@Injectable() export class ChildAService{ acompPushData = []; constructor( private _baseService: BaseService ){} pushText(pushText: string){ this._baseService.pushData("A", pushText); this.acompPushData = this._baseService.getData("A"); } }
html
<div style="background-color: azure;"> This is Acomponent <input [(ngModel)]="acompText" /> <button (click)="pushText()">Push</button> <br> りれき* <div *ngIf="_childAService.acompPushData"> <div *ngFor="let item of _childAService.acompPushData"> {{item.pushText}} </div> </div> </div>
ComponentでAComponent独自のAServiceをProvideしていますが
AserviceのConstructor上で
上位のBaseComponentでProvideされたBaseServiceを注入しています。
これを、AService/Bserviceの双方で行っているため
AとBで同一のインスタンスオブジェクトを使用している状態となります。
AComponentのPushとBComponentのPush内容が
同一のbaseServiceのbaseDataにPushされ描画されます。
Angular2で和暦フォーマットDatePipeを作る。
業務で使っているだけあって、Angular2の知見が溜まってきたので放出放出。
こんな感じの表示ができるPipeを作りました。というお話です。
使用したのは「Angular2 R.C5」でございます。
Pipeってなんぞや
Angularでデータバインドする際、下記のように記述することで
Date型の値がFormatされて出力されるようになります。
{{ nowDate | date:"yyyy/MM/d" }}
↓こんな感じに表示されます。
Date型にかぎらず
例えば数値に対応する文字列
value=10000→画面上は「福沢諭吉」と表示
value=5000→画面上は「新渡戸稲造」と表示することもできるわけです。
和暦DateFormat
.Netは和暦もフォローしてくれていますが
Webは和暦をフォローしてくれません。
なので、和暦フォーマットファンクションを作成し
それを使用したDatePipeを作成してみます。
date-jp-format.ts(V1)
export class DateJpFormat { GetJpFormatDate(value: Date, format: string): string { if (!format) format = "yyyy年 MM月 DD日 aa曜日 hh:mm:ss"; //和暦を取得 var jpDate = value.toLocaleDateString("ja-JP-u-ca-japanese"); var jpYear = jpDate.substring(0, jpDate.indexOf("/")); var jpYearArray = jpYear.match(/[0-9]+\.?[0-9]*/g); var jpYearNumber = ""; //SafariなんかはArray=Nothingでくる! if (jpYearArray) { jpYearNumber = jpYearArray[0]; } var regExpg = new RegExp(jpYearNumber, "g"); var jpYearString = jpYear.replace(regExpg, ""); var jpYearStringShort = jpYearString.slice(0, 1); //年変換(Full桁) format = format.replace("gg", jpYearString); format = format.replace("yyyy", ("" + (value.getFullYear()))); //年変換(短縮) format = format.replace("g", jpYearStringShort); format = format.replace("yy", ("0" + (value.getFullYear())).slice(-2)); format = format.replace("e", jpYearNumber); format = format.replace("MM", (("0" + (value.getMonth() + 1)).slice(-2))); format = format.replace("DD", (("0" + (value.getDay())).slice(-2))); format = format.replace("aa", weekDayList[value.getDay()]); format = format.replace("hh", (("0" + (value.getHours())).slice(-2))); format = format.replace("mm", (("0" + (value.getMinutes())).slice(-2))); format = format.replace("ss", (("0" + (value.getSeconds())).slice(-2))); return format; } } var weekDayList = ["日", "月", "火", "水", "木", "金", "土"];
とりあえず、DatePipeは抜きにして、どのように変換されるのかみてみます。
「ja-JP-u-ca-japanese」で元号「平成」等を表示してくれるのは
Chormeで、IEやEdgeなんかは、元号は取得してくれません(FireFoxは試してないです)。
Safariに至っては和暦の年数すら取得できません。
ベースはこれでいいかもしれませんが、元号が取れなかった場合のことを考え
自前で実装する他なさそうです。
Formatの実装
1. 和暦に関する情報を準備
和暦換算に必要な情報を作成します。 ・jp-calender-info.ts
export const jpCalendarInfo: JpCalenderType[] = [ { Name: "明治", StartDate: new Date("1868-09-08T00:00:00"), EndDate: new Date("1912-07-30T00:00:00") }, { Name: "大正", StartDate: new Date("1912-07-30T00:00:00"), EndDate: new Date("1926-12-25T00:00:00") }, { Name: "昭和", StartDate: new Date("1926-12-25T00:00:00"), EndDate: new Date("1989-01-08T00:00:00") }, { Name: "平成", StartDate: new Date("1989-01-08T00:00:00"), EndDate: new Date("2087-12-31T23:59:59") } ]; export class JpCalenderType{ Name: string; StartDate: Date; EndDate: Date; }
2. 和暦算出ファンクションの作成
1で作成した構造体を、算出した日付でフィルタを掛け
和暦の元号と年数の算出を行うファンクションを作成します。
private FormatJpYearString(formatDate: Date): string { var jpInfo = jpCalendarInfo .filter((x:JpCalenderType) => x.StartDate <= formatDate && formatDate < x.EndDate); return jpInfo[0].Name; } private FormatJpYearNumber(formatDate: Date): string { var jpInfo = jpCalendarInfo .filter((x:JpCalenderType) => x.StartDate <= formatDate && formatDate < x.EndDate); var nowYear = formatDate.getFullYear(); var baseYear = jpInfo[0].StartDate.getFullYear() - 1; return String(nowYear - baseYear); }
3.date-jp-format.tsの修正
上記のファンクションを合わせると、date-jp-format.tsは下記のようになります。
今回作成したファンクションは応急処置的な作りになっていますので
基本的にはブラウザ側で取得できたら、その値を使用する感じになっています。
ここで問題なのが「jpYearString 」にIEの場合ナニが入っているのか?ということです。
Emptyに見えますが実は「」という文字が入ってます。
ハイ。見えませんね。
ここにはUTF-16のU+200E文字コードの文字が入っているようです。
今回、かなり力技で出力された見えない文字をコピって貼り付けてますが
本来であれば文字コード変換等を行って、U+200E文字を潰すなりしなければならないはずです。
date-jp-format.ts
import { jpCalendarInfo, JpCalenderType } from "../function/jp-calender-info"; export class DateJpFormat { GetFormatDate(getValue: any, format: string): string { //取得した値を日付変換 var value = new Date(getValue); //Format未指定の場合は西暦表示 if (!format) format = "yyyy年 MM月 DD日 aa曜日 hh:mm:ss"; //和暦を取得 var jpYearNumber = this.FormatJpYearNumber(value); var jpYearString = this.FormatJpYearString(value); var jpYearStringShort = jpYearString.slice(0, 1); //年変換(Full桁) format = format.replace("gg", jpYearString); format = format.replace("yyyy", ("" + (value.getFullYear()))); //年変換(短縮) format = format.replace("g", jpYearStringShort); format = format.replace("yy", ("0" + (value.getFullYear())).slice(-2)); format = format.replace("e", jpYearNumber); format = format.replace("MM", (("0" + (value.getMonth() + 1)).slice(-2))); format = format.replace("DD", (("0" + (value.getDate())).slice(-2))); format = format.replace("aa", weekDayList[value.getDay()]); format = format.replace("hh", (("0" + (value.getHours())).slice(-2))); format = format.replace("mm", (("0" + (value.getMinutes())).slice(-2))); format = format.replace("ss", (("0" + (value.getSeconds())).slice(-2))); return format; } private FormatJpYearString(formatDate: Date): string { var jpInfo = jpCalendarInfo .filter((x: JpCalenderType) => x.StartDate <= formatDate && formatDate < x.EndDate); return jpInfo[0].Name; } private FormatJpYearNumber(formatDate: Date): string { var jpInfo = jpCalendarInfo .filter((x: JpCalenderType) => x.StartDate <= formatDate && formatDate < x.EndDate); var nowYear = formatDate.getFullYear(); var baseYear = jpInfo[0].StartDate.getFullYear() - 1; return String(nowYear - baseYear); } } var weekDayList = ["日", "月", "火", "水", "木", "金", "土"];
結果、Edgeでも下図のように表示されるようになりました。
4.DatePipeを作る
「@angular/core」の「Pipe」「PipeTransform 」を使用します。
@Pipe以降の「name」でHTML上に記述する、Pipeの名称を設定します。
transform()内部は表示するデータのFormatを行っている部分です。
今回は日付のデータと文字列を使用したフォーマットを行います。
結果、下記のようなソースコードとなります。
jp-date-pipe.ts
import { Pipe, PipeTransform } from "@angular/core"; import { DateJpFormat } from "../function/date-jp-format" @Pipe({ name: "JpDatePipe" }) export class JpDatePipe implements PipeTransform { dateJpFormat: DateJpFormat; transform(value: Date, args: any[]) { if (value) { this.dateJpFormat = new DateJpFormat(); var formatString = String(args); return this.dateJpFormat.GetJpFormatDate(value, formatString); } } }
5.DatePipeを使う
今回作成したPipeを使用したいコンポーネントで
「独自設定したPipeを使いますよ」と設定しなければ当然使えません。
{pipes: [JpDatePipe]}で使用するPipeを宣言することで使えるようになります。
下記のような記述で使用します。 {{nowDate | JpDatePipe:[Formatする文字列]}}
最終的に下記のようなソースコードになりました。 date-format-page.component.ts
import { Component, Pipe } from "@angular/core"; import { DateJpFormat } from "../../function/date-jp-format"; import { JpDatePipe } from "../../custom-pipe/jp-date-pipe"; @Component({ pipes: [JpDatePipe], template: ` <h2>↓変換日付↓</h2> {{nowDate}} <h2>↓DatePipe変換↓</h2> <div *ngFor="let outputFormat of formats"> {{nowDate | JpDatePipe:[outputFormat]}} </div> ` }) export class DateFormatPageComponent { nowDate: Date; formats: string[]; _dateJpFormat: DateJpFormat; constructor(){ this.nowDate = new Date(); this.formats= [ "gg", "yy", "yyyy", "gge", "ge", "gge年MM月", "yyyy年MM月", "gge年MM月DD日", "yyyy年MM月DD日", "gge年MM月DD日aa曜日", "yyyy年MM月DD日aa曜日", "gge年MM月DD日aa曜日(hh)", "yyyy年MM月DD日aa曜日(hh)", "gge年MM月DD日aa曜日(hh)", "yyyy年MM月DD日aa曜日(hh)", "gge年MM月DD日aa曜日(hh:mm)", "yyyy年MM月DD日aa曜日(hh:mm)", "gge年MM月DD日aa曜日(hh:mm:ss)", "yyyy年MM月DD日aa曜日(hh:mm:ss)", "本日、gge年MM月DD日aa曜日です!!", ]; } }
やたらと長いですが、結構な数のFormatを試しているので長くなっています。
肝の部分は下記のHTMLですね。
<h2>↓DatePipe変換↓</h2> <div *ngFor="let outputFormat of formats"> {{nowDate | JpDatePipe:[outputFormat]}} </div>
Tsで宣言したFormatをすべて表示するHTMLとなります。
JpDatePipeという一つの変数から、ありとあらゆる表示の仕方が可能となっています。
結果、Webページ上には下図のように表示されました。
まとめ
上記のとおり、日付情報自体は同じ変数を使いまわしてるので
Pipeを使えばView用の変数と登録用の変数を分ける…
なんてことはしなくても良くなるということですね。
便利。
Xamarinでもくもくしたお話し
今日、昨日と両日もくもく会に参加してました。
JXUG東京もくもく会と、業務系システム開発勉強会でもくもくしていました。
どっちもXamarinでもくもくしました。一応それらの進展を。
ListViewにViewModelをバインドしようというお話。
まずは参考
ListView Data Sources - Xamarin
ソース
まぁなんてことはない、普通のListViewバインディングです。
ViewModelとListViewのバインディングはかなり簡単に行えます。
//今回は初期ViewModelのデータをつっこんでおく var searchInfo = new ObservableCollection<SearchInfo> { new SearchInfo { Detail = "高橋 慶太郎", SearchType = ComicTokutenEnum.SearchType.AuthorSearch}, new SearchInfo { Detail = "弐瓶 勉", SearchType = ComicTokutenEnum.SearchType.AuthorSearch}, new SearchInfo { Detail = "Pumpkin Scissors", SearchType = ComicTokutenEnum.SearchType.TitleSearch}, new SearchInfo { Detail = "メイドインアビス", SearchType = ComicTokutenEnum.SearchType.TitleSearch} }; //データバインド this.BindingContext = searchInfo;
実行結果
トグルの画面が崩れるのはエミュレータゆえなのかなぁ?
ModernHttpClient + HtmlAgilityPack
XamarinでWebページを解析しよう!というお話。
こちらも比較的簡単に実現できました。
まずは参考
Xamarin のプロジェクトで NuGet を使用する : XLsoft エクセルソフト
ModernHttpClient
Nativeに依存しないWeb通信が可能となる。
NugetでGet可能。
HtmlAgilityPack
.NetのHTMLパーサーの定番。
NuGetの取得だけでも使用できるようになっているようです。
処理。
"@CH3COOHさんの処理内容にModernHttpClientの使用を追加した感じです。
ほぼ同内容の処理は下コードな感じになりました。 (パース部分はちょっと手抜きしてますけど)
・ソース
//画面EntryからCode値を取得 var code = CompanyId.Text; var urlstring = string.Format("http://stocks.finance.yahoo.co.jp/stocks/detail/?code={0}", code); var httpClient = new HttpClient(new NativeMessageHandler()); var response = await httpClient.GetAsync(urlstring); //html取得 var html = await response.Content.ReadAsStringAsync(); //AgilityPackにぶっこみ var doc = new HtmlAgilityPack.HtmlDocument(); doc.LoadHtml(html); //解析解析 var priceNode = doc.DocumentNode.Descendants("td") .Single(node => node.GetAttributeValue("class", "") == "stoksPrice"); var companyNode = doc.DocumentNode.Descendants("th") .Single(node => node.GetAttributeValue("class", "") == "symbol"); //表示 this.Outputlabel.Text = string.Format("{0}({1})の株価: {2}円", companyNode.InnerText, CompanyId.Text, priceNode.InnerHtml);
実行結果
後回しにしたこと
XMLの解析をしてみたかったのだけど、Amazonが提供しているサンプル
SignedRequestHelperがSystem.Webをバリバリ使っているんで
結構書き直さなきゃいけない印章。
もくもく会中にViewに表示できるなにかを作りたかったので
後に回しました(時間が余って作業したけど結局Amazonのは終わらなかった…)
最後に
もくもく会楽しかったのでまた参加したいです。