UIでチケットを恐れることをやめた方法

みなさんこんにちは。

開発でReactJSを使用し始めてから1年以上が経過しました。 最後に、私たちの会社がどれほど幸せになったかを共有する時が来ました。 この記事では、このライブラリを使用するようになった理由とその方法について説明します。



なぜこれすべて



私たちは小さな会社です。スタッフは約50人で、そのうち20人は開発者です。 現在、4つの開発チームがあり、それぞれに5人のフルスタック開発者がいます。 しかし、自分自身をフルスタックの開発者と呼ぶことと、SQL Serverの作業、ASP.NET、C#での開発、OOP、DDD、HTML、CSS、JSを熟知し、すべてを賢く使用できることを理解することは本当に良いことです。 もちろん、各開発者は異なるものに引き寄せられますが、私たち全員が何らかの方法で.NET開発の専門家であり、コードの90%はC#で記述されています。

当社の製品-マーケティング自動化システム-は、特定のクライアントごとに大量の設定を必要とします。 マネージャーが顧客向けに製品をカスタマイズできるようにするために、郵送を開始したり、トリガーやその他のメカニズムを作成したり、サービスをカスタマイズしたりすることができる管理サイトがあります。 この管理サイトには多くのさまざまな非自明なUIが含まれており、カスタマイズするポイントが細かくなればなるほど、運用環境でリリースする機能が増えるほど、UIは興味深いものになります。



トリガー作成




製品カテゴリで絞り込む




以前、このようなUIの開発にどのように対処しましたか? うまく対処できませんでした。 基本的に、Ajaxを受信したHTMLの一部をサーバー上でレンダリングすることに成功しました。 または、jQueryを使用したイベントでのみ。 ユーザーにとって、これは通常、継続的なダウンロード、くしゃみごとのプリローダー、奇妙なバグをもたらしました。 開発者の観点から見ると、これらは誰もが恐れていた本当のパスタでした。 計画のUIのチケットはすべて、Lの見積もりをすぐに受け取り、コードを記述するときに大量のボタンホールに注がれました。 そして、もちろん、そのようなUIに関連する多くのバグがありました。 これは次のように起こりました。最初の実装では、いくつかの小さな間違いが行われました。 そして、この奇跡のテストがなかったため、他の何かを修復するとき、必然的にバラバラになりました。

人生からの例。 これが操作作成ページです。 ビジネスについて詳しく説明しなくても、私たちとの操作は、クライアントの請負業者が使用できるRESTサービスのようなものだとしか言えません。 この操作では、消費者登録の段階に応じて可用性に制限があり、構成するために次のような制御がありました。

作成操作




そして、このコントロールの古いコードは次のとおりです。

操作可用性表示制御コード
ビューのスライス

<h2 class="column-header"> <span class="link-action" data-event-name="ToggleElements" data-event-param='{"selector":"#WorkFlowAllowance", "callback": "toggleWorkflowAvailability"}'>     </span> </h2> @Html.HiddenFor(m => m.IsAllowedForAllWorkflow, new { Id = "IsAllowedForAllWorkflow" }) <div id="WorkFlowAllowance" class="@(Model.IsAllowedForAllWorkflow ? "none" : string.Empty) row form_horizontal"> <table class="table table_hover table_control @(Model.OperationWorkflowAllowances.Any() ? String.Empty : "none")" id="operationAllowanceTable"> <thead> <tr> <th> </th> <th></th> </tr> </thead> <tbody> @Model.OperationWorkflowAllowances.Each( @<tr> <td> @item.Item.WorkflowDisplayName <input type="hidden" name="OperationTypeViewModel.OperationWorkflowAllowances[@(item.Index)].WorkflowName" value="@item.Item.WorkflowName" /> <input type="hidden" name="OperationTypeViewModel.OperationWorkflowAllowances[@(item.Index)].WorkflowDisplayName" value="@item.Item.WorkflowDisplayName" /> <input type="hidden" name="OperationTypeViewModel.OperationWorkflowAllowances[@(item.Index)].Id" value="@item.Item.Id" /> </td> <td> <button class="cell-grid__right button button_icon-only button_red removeOperationAllowance"><span class="icon icon_del"></span></button> <span class="cell-grid__wraps">@(item.Item.StageName ?? "")</span> <input type="hidden" name="OperationTypeViewModel.OperationWorkflowAllowances[@(item.Index)].StageName" value="@item.Item.StageName" /> <input type="hidden" name="OperationTypeViewModel.OperationWorkflowAllowances[@(item.Index)].StageDisplayName" value="@item.Item.StageDisplayName" /> </td> </tr>) </tbody> </table> <div class="col col_462"> <div class="form-group form-group_all"> </div> @if (Model.WorkFlows.Any()) { <div> <div class="form-group"> <label class="form-label"><span> </span></label> @Html.DropDownList("WorkflowList", Model.WorkFlows, new Dictionary<string, object> { { "class", "form-control select2 w470" }, { "data-placeholder", "  " }, { "id", "workflowList" }, { "disabled", "disabled" } }) </div> <div class="form-group"> <div class="form-list"> <input id="isAllowedForAllStagesForCurrentWorkflow" type="checkbox" name="StageMechanicsRegistratioName" autocomplete="off"> <label for="isAllowedForAllStagesForCurrentWorkflow">     <span id="exceptAnonymus"></span><span id="workflowName"></span></label> </div> </div> <div class="form-group"> <label class="form-label"><span></span></label> @Html.DropDownList("WorkflowStageList", new SelectListItem[0], new Dictionary<string, object> { { "class", "form-control select2 w470" }, { "data-placeholder", "  " }, { "id", "workflowStageList" }, { "disabled", "disabled"} }) </div> <div class="form-group"> <button class="button button_blue" id="addOperationAllowance"> </button> </div> </div> } else { @:     } </div> </div>
      
      





そして、このビューを機能させたjsは次のとおりです(実行可能なコードを表示することを目的としていたのではなく、それがどれほど悲しかったかを示しています)。

 function initOperationAllowance(typeSelector) { $('#workflowList').prop('disabled', false); $('#workflowList').trigger('change'); if ($(typeSelector).val() == 'PerformAction') { $('#exceptAnonymus').html('( )'); } else { $('#exceptAnonymus').html(''); } } function toggleWorkflowAvailability() { var element = $("#IsAllowedForAllWorkflow"); $('#operationAllowanceTable tbody tr').remove(); parameters.selectedAllowances = []; return element.val().toLowerCase() == 'true' ? element.val(false) : element.val(true); } function deleteRow(row) { var index = getRowIndex(row); row.remove(); parameters.selectedAllowances.splice(index, 1); $('#operationAllowanceTable input').each(function () { var currentIndex = getFieldIndex($(this)); if (currentIndex > index) { decrementIndex($(this), currentIndex); } }); if (parameters.selectedAllowances.length == 0) { $('#operationAllowanceTable').hide(); } } function updateWorkflowSteps(operationType) { var workflow = $('#workflowList').val(); if (workflow == '') { $('#isAllowedForAllStagesForCurrentWorkflow') .prop('checked', false) .prop('disabled', 'disabled'); refreshOptionList( $('#workflowStageList'), [{ Text: '  ', Value: '', Selected: true }] ); $('#workflowStageList').trigger('change').select2('enable', false); return; } var url = parameters.stagesUrlTemplate + '?workflowName=' + workflow + '&OperationTypeName=' + operationType; $.getJSON(url, null, function (data) { $('#isAllowedForAllStagesForCurrentWorkflow') .prop('checked', false) .removeProp('disabled'); refreshOptionList($('#workflowStageList'), data); $('#workflowStageList').trigger('change').select2('enable', true); }); } function refreshOptionList(list, data) { list.find('option').remove(); $.each(data, function (index, itemData) { var option = new Option(itemData.Text, itemData.Value, null, itemData.Selected); list[0].add(option); }); } function AddRow(data) { var rowsCount = $('#operationAllowanceTable tr').length; var index = rowsCount - 1; var result = '<tr ' + (rowsCount % 2 != 0 ? 'class="bgGray">' : '>') + '<td>' + '{DisplayWorkflowName}' + '<input type="hidden" name="OperationTypeViewModel.OperationWorkflowAllowances[' + index + '].WorkflowName" value="{WorkflowName}"/>' + '<input type="hidden" name="OperationTypeViewModel.OperationWorkflowAllowances[' + index + '].Id" value=""/>' + '<input type="hidden" name="OperationTypeViewModel.OperationWorkflowAllowances[' + index + '].WorkflowDisplayName" value="{DisplayWorkflowName}"/>' + '</td>' + '<td>' + '<button class="cell-grid__right button button_icon-small button_red removeOperationAllowance"><span class="icon icon_del"></span></button>' + '<span class="cell-grid__wraps">{DisplayStageName}</span>' + '<input type="hidden" name="OperationTypeViewModel.OperationWorkflowAllowances[' + index + '].StageName" value="{StageName}"/>' + '<input type="hidden" name="OperationTypeViewModel.OperationWorkflowAllowances[' + index + '].StageDisplayName" value="{DisplayStageName}"/>' + '</td>' + '</tr>'; for (key in data) { result = result.replace(new RegExp('{' + key + '}', 'g'), data[key]); } $('#operationAllowanceTable').show().append(result); } function IsValidForm() { var result = ValidateList($('#workflowList'), '    ') & ValidateListWithCheckBox($('#workflowStageList'), $('#isAllowedForAllStagesForCurrentWorkflow'), '     '); if (!result) return false; var workflowName = $('#workflowList').val(); var stageName = ''; if (!$('#isAllowedForAllStagesForCurrentWorkflow').is(':checked')) { stageName = $('#workflowStageList').val(); } hideError($('#workflowList')); hideError($('#workflowStageList')); for (var i = 0; i < parameters.selectedAllowances.length; i++) { if (parameters.selectedAllowances[i].workflow == workflowName && parameters.selectedAllowances[i].stage == stageName) { if (stageName == '') { showError($('#workflowList'), '      '); } else { showError($('#workflowStageList'), '     '); } result = false; } else if (parameters.selectedAllowances[i].workflow == workflowName && parameters.selectedAllowances[i].stage == '') { showError($('#workflowList'), '      '); result = false; } } return result; } function ValidateList(field, message) { if (field.val() == "") { showError(field, message); return false; } hideError(field); return true; } function ValidateListWithCheckBox(field, checkBoxField, message) { if (!checkBoxField.prop('checked')) { return ValidateList(field, message); } hideError(field); return true; } function showError(field, message) { if (typeof (message) === 'undefined') { message = '   '; } field.addClass('input-validation-error form-control_error'); field.parent('.form-group').find('div.tooltip-error').remove(); field.closest('.form-group').append( '<div class="tooltip-icon tooltip-icon_error"><div class="tooltip-icon__content">' + '<strong></strong><br>' + message + '</div></div>'); } function hideError(field) { field.removeClass('input-validation-error form-control_error'); field.parent('.form-group').find('div.tooltip-icon_error').remove(); } function getRowIndex(row) { return getFieldIndex(row.find('input:first')); } function getFieldIndex(field) { var name = field.prop('name'); var startIndex = name.indexOf('[') + 1; var endIndex = name.indexOf(']'); return name.substr(startIndex, endIndex - startIndex); } function decrementIndex(field, index) { var name = field.prop('name'); var newIndex = index - 1; field.prop('name', name.replace('[' + index + ']', '[' + newIndex + ']')); } function InitializeWorkflowAllowance(settings) { $(function() { parameters.selectedAllowances = settings.selectedAllowances; initOperationAllowance(parameters.typeSelector); $('#workflowList').change(function () { updateWorkflowSteps($(parameters.typeSelector).val()); }); $('#addOperationAllowance').click(function (event) { event.preventDefault(); if (IsValidForm()) { var data = { 'StageName': $('#workflowStageList').val(), 'WorkflowName': $('#workflowList').val(), }; if ($('#isAllowedForAllStagesForCurrentWorkflow').is(':checked')) { data.DisplayWorkflowName = $('#workflowList option[value=' + data.WorkflowName + ']').text(); data.DisplayStageName = ''; data.StageName = ''; } else { data.DisplayWorkflowName = $('#workflowList option[value=' + data.WorkflowName + ']').text(); data.DisplayStageName = $('#workflowStageList option[value=' + data.StageName + ']').text(); } AddRow(data); if (data.StageName == '') { var indexes = []; //      for (var i = 0; i < parameters.selectedAllowances.length; i++) { if (parameters.selectedAllowances[i].workflow == data.WorkflowName) { indexes.push(i); } } $("#operationAllowanceTable tbody tr").filter(function (index) { return $.inArray(index, indexes) > -1; }).each(function () { deleteRow($(this)); }); } parameters.selectedAllowances.push({ workflow: data.WorkflowName, stage: data.StageName }); $("#workflowList").val('').trigger('change'); updateWorkflowSteps($(parameters.typeSelector).val()); } }); $('#isAllowedForAllStagesForCurrentWorkflow').click(function () { if ($(this).is(":checked")) { $('#workflowStageList').prop('disabled', 'disabled'); } else { $('#workflowStageList').removeProp('disabled'); } }); $('#operationAllowanceTable').on('click', 'button.removeOperationAllowance', function (event) { var row = $(this).parent().parent(); setTimeout(function () { deleteRow(row); }, 20); event.preventDefault(); }); });
      
      









新しい希望



ある時点で、そのように生きることはもはや不可能であることに気づきました。 いくつかの議論の後、フロントエンドを理解し、真の道を指示する側の人が必要であるという結論に達しました。 Reactの使用を提案するフリーランサーを雇いました。 彼は私たちとあまり仕事をしていませんでしたが、何が起こっているのかを示すためにいくつかのコントロールを作成することができました。 公式ウェブサイトのチュートリアルを完了してからReactが本当に好きでしたが、誰もがそれを気に入りませんでした。 さらに、筋金入りのフロントエンド開発者はjavascriptを愛していますが、静的な型の開発の世界では、javascriptは(軽度に言えば)人気がないため、使用するように提供されたこれらのWebパックやうなり声はすべて怖がっています。 その結果、対処する必要があるフレームワークを決定するために、異なるフレームワークを使用して、複雑なUIのいくつかのプロトタイプを作成することが決定されました。 選択した各フレームワークのサポーターは、コードを比較できるように同じコントロールのプロトタイプを作成する必要がありました。 Angular、React、Knockoutを比較しました。 後者はプロトタイプ段階を経ることさえありませんでしたし、私はどんな理由で覚えていることすらありません。 しかし、AngularとReactの支持者の間で、会社は真の内戦を開始しました!

冗談:)実際、各フレームワークには1人のサポーターがいましたが、他の誰もがどちらも好きではありませんでした。 誰もがためらい、何も決められなかった。 Angularでは、誰もがその複雑さに悩まされ、Reactでは、その当時のVisual Studioでのサポートの欠如が本当に非常に不愉快な事実であった、愚かな構文でした。

幸いなことに、私たちの上司(会社の所有者の1人)が私たちを助けてくれました。もちろん、長い間プログラムしていませんでしたが、彼の指は脈動を保っています。 プロトタイプが効果をもたらさないことが明らかになり、開発が何らかの理由で時間を浪費するようになった後(その時点で、比較のためのコードを増やすために、さらに大きなサイズの別のプロトタイプを作成する計画を立てていました!)、彼。 さて、なぜ彼の選択がまだReactに落ちたかを思い出して、Sasha agornik Gornikは私に次のように言った(私はホリバーのためではなく彼の言葉を引用する、これは単なる意見である。 :

いくつかのプロトタイプがありました:反応、角度、および他のもの。 見ました。 私は角が好きではなく、反応が好きでした。

しかし、[大声で]大声で叫び、他のみんなは野菜のようでした。 読んで見なければなりませんでした。

この反応は、多くのクールなサイトで生産されていることがわかりました。 FB、Yahoo、WhatsAppなどがあります。 明らかに巨大な採用が来ており、未来があります。

そして格納庫で-[何も良いことはありません]。 未来を見ました。 2.0で強化したいアングルのプロトタイプで、私が好まなかったものはすべて見ました。

反応は人生のために作られたものであり、特定の問題を解決するものであることに気づいた そして角度-Googleの脳からの理論家はあごひげを生やし、あらゆる種類の概念を思いつきます。 GWTまたはそれが何であれ、そうでした。

まあ、私は強い意志で野菜の味方をする必要があることに気づきました、さもなければ派手で間違ったものが勝つでしょう。 これを行う前に、3,300万の証拠とリンクをチャンネルに投げ込み、[チーフアーキテクト]の支援を求め、誰も夢中にならないように努めました。

また、地獄のような重要な議論を思い出しました。 反応のために、それを段階的に実行して既存のページにねじ込む美しい方法と、それらを完全にやり直すのに必要な角度があり、これも[貧しい]アーキテクチャで修正されます。

それから私はまた、反応として、理論的には、UIがWebに対しても実行できることを読みました。 そして、すべてのサーバー側のjs /そこに反応し、それがすべて行く場所。 そして最後に、あなたは単一の議論を取ることができませんでした。

スタジオのサポートはすぐに削減されることに気付きました。 最終的に、すべてがまったく同じように起こりました。 私は確かにこの決定にとてもうれしいです)


どうしたの?



カードを公開し、UIをどのように調理しているかを示します。 もちろん、フロントエンドのアーティストは今すぐ笑い始めますが、私たちにとってこのコードは本当の勝利であり、とても満足しています:)

たとえば、追加のフィールドを作成するためにページを使用します。 簡単なビジネス参照:消費者、注文、購入、製品などの一部のエンティティには、顧客固有のデータが関連付けられている場合があります。 そのようなデータを保存するために、従来のエンティティ-属性-値モデルを使用します 。 最初は、各クライアントの追加フィールドが(開発時間を節約するために)データベースに直接入力されましたが、最後に、UIの時間も見つかりました。

プロジェクトにフィールドを追加するページは次のとおりです。

列挙型のフィールドを追加する




String型のフィールドを追加する




そして、React上でこのページのコードは次のようになります。

追加のフィールドを追加/編集するためのページのコンポーネント
 /// <reference path="../../references.d.ts"/> module DirectCrm { export interface SaveCustomFieldKindComponentProps extends Model<CustomFieldKindValueBackendViewModel> { } interface SaveCustomFieldKindComponentState { model?: CustomFieldKindValueBackendViewModel; validationContext: IValidationContext<CustomFieldKindValueBackendViewModel>; } export class SaveCustomFieldKindComponent extends React.Component<SaveCustomFieldKindComponentProps, SaveCustomFieldKindComponentState> { private _componentsMap: ComponentsMap<CustomFieldKindConstantComponentDataBase, CustomFieldKindTypedComponentProps>; constructor(props: SaveCustomFieldKindComponentProps) { super(props); this.state = { model: props.model, validationContext: createTypedValidationContext<CustomFieldKindValueBackendViewModel>(props.validationSummary) }; this._componentsMap = ComponentsMap.initialize(this.state.model.componentsMap); } _setModel = (model: CustomFieldKindValueBackendViewModel) => { this.setState({ model: model }); } _handleFieldTypeChange = (newFieldType: string) => { var clone = _.clone(this.state.model); clone.fieldType = newFieldType; clone.typedViewModel = { type: newFieldType, $type: this._componentsMap[newFieldType].viewModelType }; this._setModel(clone); } _getColumnPrefixOrEmptyString = (entityType: string) => { var entityTypeDto = _.find(this.props.model.entityTypes, et => et.systemName === entityType); return entityTypeDto && entityTypeDto.prefix || ""; } _hanleEntityTypeChange = (newEntityType: string) => { var clone = _.clone(this.state.model); clone.entityType = newEntityType; var columnPrefix = this._getColumnPrefixOrEmptyString(newEntityType); clone.columnName = `${columnPrefix}${this.state.model.systemName || ""}`; this._setModel(clone); } _handleSystemNameChange = (newSystemName: string) => { var clone = _.clone(this.state.model); clone.systemName = newSystemName; var columnPrefix = this._getColumnPrefixOrEmptyString(this.state.model.entityType); clone.columnName = `${columnPrefix}${newSystemName || ""}`; this._setModel(clone); } _renderComponent = () => { var entityTypeSelectOptions = this.state.model.entityTypes.map(et => { return { Text: et.name, Value: et.systemName } }); var fieldTypeSelectOptions = Object.keys(this._componentsMap). map(key => { return { Text: this._componentsMap[key].name, Value: key }; }); var componentInfo = this._componentsMap[this.state.model.fieldType]; var TypedComponent = componentInfo.component; return ( <div> <div className="row form_horizontal"> <FormGroup label=" " validationMessage={this.state.validationContext.getValidationMessageFor(m => m.entityType)}> <div className="form-control"> <Select value={this.state.model.entityType} options={entityTypeSelectOptions} width="normal" placeholder=" " onChange={this._hanleEntityTypeChange} /> </div> </FormGroup> <DataGroup label=" " value={this.state.model.columnName} /> <FormGroup label="" validationMessage={this.state.validationContext.getValidationMessageFor(m => m.name)}> <Textbox value={this.state.model.name} width="normal" onChange={getPropertySetter( this.state.model, this._setModel, viewModel => viewModel.name)} /> </FormGroup> <FormGroup label=" " validationMessage={this.state.validationContext.getValidationMessageFor(m => m.systemName)}> <Textbox value={this.state.model.systemName} width="normal" onChange={this._handleSystemNameChange} /> </FormGroup> <FormGroup label=" " validationMessage={this.state.validationContext.getValidationMessageFor(m => m.fieldType)}> <div className="form-control"> <Select value={this.state.model.fieldType} options={fieldTypeSelectOptions} width="normal" placeholder=" " onChange={this._handleFieldTypeChange} /> </div> </FormGroup> <TypedComponent validationContext={this.state.validationContext.getValidationContextFor(m => m.typedViewModel)} onChange={getPropertySetter( this.state.model, this._setModel, viewModel => viewModel.typedViewModel)} value={this.state.model.typedViewModel} constantComponentData={componentInfo.constantComponentData} /> <FormGroup> <Checkbox checked={this.state.model.isMultiple} label="       " onChange={getPropertySetter( this.state.model, this._setModel, viewModel => viewModel.isMultiple)} disabled={false} /> </FormGroup> {this._renderShouldBeExportedCheckbox()} </div> </div>); } _getViewModelValue = () => { var clone = _.clone(this.state.model); clone.componentsMap = null; clone.entityTypes = null; return clone; } render() { return ( <div> <fieldset> {this._renderComponent() } </fieldset> <HiddenInputJsonSerializer model={this._getViewModelValue()} name={this.props.modelName} /> </div>); } _renderShouldBeExportedCheckbox = () => { if (this.state.model.entityType !== "HistoricalCustomer") return null; return ( <FormGroup validationMessage={this.state.validationContext.getValidationMessageFor(m => m.shouldBeExported)}> <Checkbox checked={this.state.model.shouldBeExported} label="   " onChange={getPropertySetter( this.state.model, this._setModel, viewModel => viewModel.shouldBeExported)} disabled={false} /> </FormGroup>); } } }
      
      







TypeScript



「それは何でしたか?」javascriptが表示されることを期待しているかどうかを尋ねることができます。 これはtsxです-TypeScriptでのReactのjsxのバリアントです。 UIは完全に静的に型付けされており、「マジックライン」はありません。 同意します、これは私たちのような筋金入りのバックエンドから期待できます:)

いくつかの言葉があります。 私は静的および動的に型付けされた言語のトピックでホリバーを上げるという目標はありません。 私たちの会社では、動的言語を好む人はいませんでした。 私たちは、長年にわたってリファクタリングされてきた大規模なサポートプロジェクトを作成することはそれほど難しくないと考えています。 IntelliSenseが機能しないため、書くのは難しいです:)これが私たちの信念です。 すべてをテストでカバーできると主張することができ、それから動的に型付けされた言語で可能になりますが、このトピックについては議論しません。

tsx形式は、スタジオと別の非常に重要なポイントである新しいR#でサポートされています。 しかし、1年前(R#とは異なり)スタジオではjsxのサポートさえありませんでした。jsの開発には別のコードエディターが必要でした(SublimeとAtomを使用しました)。 この結果、スタジオソリューションではファイルの半分が十分ではなく、肉屋が追加されただけでした。 幸福はすでに来ているので、それについては話しましょう。

純粋な形式のtypescriptでさえ、私たちが望む静的型付けのレベルを与えないことに注意すべきです。 たとえば、モデルにいくつかのプロパティを設定する場合(実際にUIコントローラーをいくつかのモデルプロパティにバインドするため)、そのようなプロパティごとに長い間コールバック関数を記述し、プロパティの名前を取るコールバックを1つ使用できます。静的に入力されることはありません。 具体的には、この問題をおよそこのコードで解決しました(上記のgetPropertySetterの使用例を参照できます)。

 /// <reference path="../../libraries/underscore.d.ts"/> function getPropertySetter<TViewModel, TProperty>( viewModel: TViewModel, viewModelSetter: {(viewModel: TViewModel): void}, propertyExpression: {(viewModel: TViewModel): TProperty}): {(newPropertyValue: TProperty): void} { return (newPropertyValue: TProperty) => { var viewModelClone = _.clone(viewModel); var propertyName = getPropertyNameByPropertyProvider(propertyExpression); viewModelClone[propertyName] = newPropertyValue; viewModelSetter(viewModelClone); }; } function getPropertyName<TObject>(obj: TObject, expression: {(obj: TObject): any}): string { return getPropertyNameByPropertyProvider(expression); } function getPropertyNameByPropertyProvider(propertyProvider: Function): string { return /\.([^\.;]+);?\s*\}$/.exec(propertyProvider.toString())[1]; }
      
      





getPropertyNameByPropertyProviderの実装が非常に馬鹿げていることは間違いありません(別の単語を選ぶことすらありません)。 しかし、typescriptはまだ別の選択肢を提供しません。 ExpressionTreeとnameofは含まれていません。getPropertySetterのプラスの側面は、このような実装のマイナスの側面を上回ります。 最後に、彼女に何が起こる可能性がありますか? ある時点で速度が低下し始める可能性があり、そこにキャッシュを割り当てることができます。または、その頃にはtypescriptのnameofが実行されます。

このようなハックのおかげで、たとえば、コード全体で名前を変更しているので、どこかで何かがバラバラになることを心配する必要はありません。

そうでなければ、すべてが魔法のように機能します。 コンポーネントに必要な小道具を指定しませんでしたか? コンパイルエラー。 間違ったタイプのプロップをコンポーネントに渡しましたか? コンパイルエラー。 実行時の警告付きの愚かなPropTypeはありません。 ここでの唯一の問題は、typescriptではなくC#でバックエンドを保持していることです。そのため、クライアントで使用される各モデルは、サーバーとクライアントで2回記述する必要があります。 ただし、この問題には解決策があります。私たちは、.NETの型からのtypescriptのプロトタイプ型ジェネレーターを作成しました。 このユーティリティを何らかの方法で適用し、戦闘状態での動作を確認する必要があるようです。 どうやら、すべてがすでに大丈夫です。



コンポーネントレンダリング



ページを開くときにコンポーネントを初期化する方法と、サーバーコードと対話する方法について詳しく説明します。 カプリングが非常に高いことをすぐに警告しますが、何ができますか。

サーバー上の各コンポーネントには、POST要求中にこのコンポーネントがバインドするビューモデルがあります。 通常、最初からコンポーネントを初期化するために同じビューモデルが使用されます。 たとえば、上記の追加フィールドページのビューモデルを初期化するコード(C#)は次のとおりです。

サーバー上のモデル初期化コードを表示する
 public void PrepareForViewing(MvcModelContext mvcModelContext) { ComponentsMap = ModelApplicationHostController .Instance .Get<ReactComponentViewModelConfiguration>() .GetNamedObjectRelatedComponentsMapFor<CustomFieldKindTypedViewModelBase, CustomFieldType>( customFieldViewModel => customFieldViewModel.PrepareForViewing(mvcModelContext)); EntityTypes = ModelApplicationHostController.NamedObjects .GetAll<CustomFieldKindEntityType>() .Select( type => new EntityTypeDto { Name = type.Name, SystemName = type.SystemName, Prefix = type.ColumnPrefix }) .ToArray(); if (ModelApplicationHostController.NamedObjects.Get<DirectCrmFeatureComponent>().Sku.IsEnabled()) { EntityTypes = EntityTypes.Where( et => et.SystemName != ModelApplicationHostController.NamedObjects .Get<CustomFieldKindEntityTypeComponent>().Purchase.SystemName) .ToArray(); } else { EntityTypes = EntityTypes.Where( et => et.SystemName != ModelApplicationHostController.NamedObjects .Get<CustomFieldKindEntityTypeComponent>().Sku.SystemName) .ToArray(); } if (FieldType.IsNullOrEmpty()) { TypedViewModel = new StringCustomFieldKindTypedViewModel(); FieldType = TypedViewModel.Type; } }
      
      







ここでは、いくつかのプロパティとコレクションが初期化され、リストを作成するために使用されます。

このビューモデルのデータを使用してコンポーネントを描画するために、ExtensionメソッドHtmlHelperが記述されています。 実際、コンポーネントをレンダリングする必要がある場所では、次のコードを使用します。

 @Html.ReactJsFor("DirectCrm.SaveCustomFieldKindComponent", m => m.Value)
      
      





最初のパラメーターはコンポーネントの名前、2番目はPropertyExpression-このコンポーネントのデータが配置されているページのビューモデルのパスです。 このメソッドのコードは次のとおりです。

 public static IHtmlString ReactJsFor<TModel, TProperty>( this HtmlHelper<TModel> htmlHelper, string componentName, Expression<Func<TModel, TProperty>> expression, object initializeObject = null) { var validationData = htmlHelper.JsonValidationMessagesFor(expression); var metadata = ModelMetadata.FromLambdaExpression(expression, htmlHelper.ViewData); var modelData = JsonConvert.SerializeObject( metadata.Model, new JsonSerializerSettings { TypeNameHandling = TypeNameHandling.Auto, TypeNameAssemblyFormat = FormatterAssemblyStyle.Full, Converters = { new StringEnumConverter() } }); var initializeData = JsonConvert.SerializeObject(initializeObject); return new HtmlString(string.Format( "<div data-react-component='{0}' data-react-model-name='{1}' data-react-model='{2}' " + "data-react-validation-summary='{3}' data-react-initialize='{4}'></div>", HttpUtility.HtmlEncode(componentName), HttpUtility.HtmlEncode(htmlHelper.NameFor(expression)), HttpUtility.HtmlEncode(modelData), HttpUtility.HtmlEncode(validationData), HttpUtility.HtmlEncode(initializeData))); }
      
      





, div, , : , , , , , - . div :

 function initializeReact(context) { $('div[data-react-component]', context).each(function () { var that = this; var data = $(that).data(); var component = eval(data.reactComponent); if (data.reactInitialize == null) { data.reactInitialize = {}; } var props = $.extend({ model: data.reactModel, validationSummary: data.reactValidationSummary, modelName: data.reactModelName }, data.reactInitialize); React.render( React.createElement(component, props), that ); }); }
      
      





, — state. , ( / select').



Binding



, , ?

. . . , , ( , ), hidden input, , json. , json ASP.NET , ModelBinder.

hidden input'. :

 <HiddenInputJsonSerializer model={this._getViewModelValue() } name={this.props.modelName} />
      
      





:

 class HiddenInputJsonSerializer extends React.Component<{ model: any, name: string }, {}> { render() { var json = JSON.stringify(this.props.model); var name = this.props.name; return ( <input type="hidden" value={json} name={name} /> ); } }
      
      





— json , this.props.modelName — , data-react-model-name (. ), - -, json'.

, json - , . , -, json', JsonBindedAttribute. -, -, json:

 public class CustomFieldKindCreatePageViewModel : AdministrationSiteMasterViewModel { public CustomFieldKindCreatePageViewModel() { Value = new CustomFieldKindValueViewModel(); } [JsonBinded] public CustomFieldKindValueViewModel Value { get; set; } ///      - }
      
      





, - CustomFieldKindCreatePageViewModel.Value . - — ModelBinder. : JsonBindedAttribute — , CustomFieldKindValueViewModel ( ). :

, json
 public class MindboxDefaultModelBinder : DefaultModelBinder { private object DeserializeJson( string json, Type type, string fieldNamePrefix, ModelBindingContext bindingContext, ControllerContext controllerContext) { var settings = new JsonSerializerSettings { TypeNameHandling = TypeNameHandling.Auto, MetadataPropertyHandling = MetadataPropertyHandling.ReadAhead, Converters = new JsonConverter[] { new ReactComponentPolimorphicViewModelConverter(), new FormBindedConverter(controllerContext, bindingContext, fieldNamePrefix) } }; return JsonConvert.DeserializeObject(json, type, settings); } protected override void BindProperty( ControllerContext controllerContext, ModelBindingContext bindingContext, PropertyDescriptor propertyDescriptor) { if (!propertyDescriptor.Attributes.OfType<JsonBindedAttribute>().Any()) { base.BindProperty(controllerContext, bindingContext, propertyDescriptor); } } public override object BindModel(ControllerContext controllerContext, ModelBindingContext bindingContext) { var result = base.BindModel(controllerContext, bindingContext); // ... // ,      // ... if (result != null) { FillJsonBindedProperties(controllerContext, bindingContext, result); } return result; } private static string BuildFormVariableFullName(string modelName, string formVariableName) { return modelName.IsNullOrEmpty() ? formVariableName : string.Format("{0}.{1}", modelName, formVariableName); } private void FillJsonBindedProperties( ControllerContext controllerContext, ModelBindingContext bindingContext, object result) { var jsonBindedProperties = result.GetType().GetProperties() .Where(pi => pi.HasCustomAttribute<JsonBindedAttribute>()) .ToArray(); foreach (var propertyInfo in jsonBindedProperties) { var formFieldFullName = BuildFormVariableFullName( bindingContext.FallbackToEmptyPrefix ? string.Empty : bindingContext.ModelName, propertyInfo.Name); if (controllerContext.HttpContext.Request.Params.AllKeys.Contains(formFieldFullName)) { var json = controllerContext.HttpContext.Request.Params[formFieldFullName]; if (!json.IsNullOrEmpty()) { var convertedObject = DeserializeJson( json, propertyInfo.PropertyType, formFieldFullName, bindingContext, controllerContext); propertyInfo.SetValue(result, convertedObject); } } else { throw new InvalidOperationException( string.Format( "    property {0}   {1}.  99.9%      js.", formFieldFullName, result.GetType().AssemblyQualifiedName)); } } } }
      
      







, , json, json , , 99.9% - , - . , .

, , html, , react- . , - react', , react'. , , . , :





, «», js , react — , . js, , html , js . , , UI-, , react. « », , react' .

? , , input' name, , react. input' hidden input', - . , POST-, , -, , JsonBindedAttribute, , json. , - , FormBindedAttribute, json FormBindedConverter, :

FormBindedConverter
 public class FormBindedConverter : JsonConverter { private readonly ControllerContext controllerContext; private readonly ModelBindingContext parentBindingContext; private readonly string formNamePrefix; private Type currentType = null; private static readonly Type[] primitiveTypes = new[] { typeof(int), typeof(bool), typeof(long), typeof(decimal), typeof(string) }; public FormBindedConverter( ControllerContext controllerContext, ModelBindingContext parentBindingContext, string formNamePrefix) { this.controllerContext = controllerContext; this.parentBindingContext = parentBindingContext; this.formNamePrefix = formNamePrefix; } public override bool CanConvert(Type objectType) { return currentType != objectType && !primitiveTypes.Contains(objectType); } public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer) { var currentJsonPath = reader.Path; currentType = objectType; var result = serializer.Deserialize(reader, objectType); currentType = null; if (result == null) return null; var resultType = result.GetType(); var formBindedProperties = resultType.GetProperties().Where(p => p.HasCustomAttribute<FormBindedAttribute>()); foreach (var formBindedProperty in formBindedProperties) { var formBindedPropertyName = formBindedProperty.Name; var formBindedPropertyFullPath = $"{formNamePrefix}.{currentJsonPath}.{formBindedPropertyName}"; var formBindedPropertyModelBinderAttribute = formBindedProperty.PropertyType.TryGetSingleAttribute<ModelBinderAttribute>(); var effectiveBinder = GetBinder(formBindedPropertyModelBinderAttribute); var formBindedObject = effectiveBinder.BindModel( controllerContext, new ModelBindingContext(parentBindingContext) { ModelMetadata = ModelMetadataProviders.Current.GetMetadataForType( () => formBindedProperty.GetValue(result), formBindedProperty.PropertyType), ModelName = formBindedPropertyFullPath }); formBindedProperty.SetValue(result, formBindedObject); } return result; } private static IModelBinder GetBinder(ModelBinderAttribute formBindedPropertyModelBinderAttribute) { IModelBinder effectiveBinder; if (formBindedPropertyModelBinderAttribute == null) { effectiveBinder = new MindboxDefaultModelBinder(); } else { effectiveBinder = formBindedPropertyModelBinderAttribute.GetBinder(); } return effectiveBinder; } public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer) { serializer.Serialize(writer, value); } }
      
      







- json, FormBindedAttribute. - , , binder , binder .

MindboxDefaultModelBinder, FormBindedConverter, FilterViewModelBinder, MindboxDefaultModelBinder.



-



UI , . :













UI. , switch , . , , . :

 module DirectCrm { export class StringCustomFieldKindComponent extends CustomFieldKindComponentBase { render() { var stringViewModel = this.props.value as StringCustomerFieldKindTypedBackendViewModel; var stringConstantData = this.props.constantComponentData as StringCustomFieldKindConstantComponentData; var validationContext = this.props.validationContext as IValidationContext<StringCustomerFieldKindTypedBackendViewModel>; return ( <div> {super.render() } <FormGroup label="  " validationMessage={validationContext.getValidationMessageFor(m => m.validationStrategySystemName) } > <div className="form-control"> <Commons.Select value={stringViewModel.validationStrategySystemName} width="normal" onChange={getPropertySetter( stringViewModel, vm => this.props.onChange(vm), m => m.validationStrategySystemName) } options={stringConstantData.validationStrategies} disabled={this.props.disabled}/> </div> </FormGroup> </div>); } } }
      
      





 module DirectCrm { export class DefaultCustomFieldKindComponent extends CustomFieldKindComponentBase { } }
      
      





 module DirectCrm { export class CustomFieldKindComponentBase extends React.Component<DirectCrm.CustomFieldKindTypedComponentProps, {}> { render() { return <FormGroup label = " " validationMessage = { this.props.validationMessageForFieldType } > <div className="form-control"> <Commons.Select value={this.props.fieldType} options={this.props.fieldTypeSelectOptions} width="normal" placeholder=" " onChange={this.props.handleFieldTypeChange} disabled = {this.props.disabled}/> </div> {this.renderTooltip() } </FormGroup> } renderTooltip() { return <Commons.Tooltip additionalClasses="tooltip-icon_help" message={this.props.constantComponentData.tooltipMessage }/> } } }
      
      





?

, :

 _renderComponent = () => { var fieldTypeSelectOptions = Object.keys(this._componentsMap). map(key => { return { Text: this._componentsMap[key].name, Value: key }; }); var componentInfo = this._componentsMap[this.state.model.fieldType]; var TypedComponent = componentInfo.component; return ( <div> <div className="row form_horizontal"> <div className="col-group"> //    <TypedComponent validationContext={this.state.validationContext.getValidationContextFor(m => m.typedViewModel) } onChange={getPropertySetter( this.state.model, this._setModel, viewModel => viewModel.typedViewModel) } value={this.state.model.typedViewModel} fieldType={this.state.model.fieldType} validationMessageForFieldType={this.state.validationContext.getValidationMessageFor(m=> m.fieldType) } fieldTypeSelectOptions={fieldTypeSelectOptions} handleFieldTypeChange={this._handleFieldTypeChange} constantComponentData={componentInfo.constantComponentData} disabled={!this.state.model.isNew}/> </div> //    </div>); }
      
      





, TypedComponent, _componentsMap. _componentsMap — , ( « ») componentInfo, , : , (, url- - ), .NET , , -. _componentsMap json :

ComponentsMap'
 "componentsMap":{ "Integer":{ "name":"", "viewModelType":"Itc.DirectCrm.Web.IntegerCustomFieldKindTypedViewModel, itc.DirectCrm.Web, Version=6.459.0.3741, Culture=neutral, PublicKeyToken=null", "componentName":"DirectCrm.DefaultCustomFieldKindComponent", "constantComponentData":{ "$type":"Itc.DirectCrm.Web.IntegerCustomFieldKindTypedViewModel, itc.DirectCrm.Web, Version=6.459.0.3741, Culture=neutral, PublicKeyToken=null", "tooltipMessage":": 123456", "type":"Integer" } }, "String":{ "name":"", "viewModelType":"Itc.DirectCrm.Web.StringCustomFieldKindTypedViewModel, itc.DirectCrm.Web, Version=6.459.0.3741, Culture=neutral, PublicKeyToken=null", "componentName":"DirectCrm.StringCustomFieldKindComponent", "constantComponentData":{ "$type":"Itc.DirectCrm.Web.StringCustomFieldKindTypedViewModel, itc.DirectCrm.Web, Version=6.459.0.3741, Culture=neutral, PublicKeyToken=null", "validationStrategies":[ { "Disabled":false, "Group":null, "Selected":true, "Text":" ", "Value":"Default" }, { "Disabled":false, "Group":null, "Selected":false, "Text":"    ", "Value":"IsValidLatinStringWithWhitespaces" }, { "Disabled":false, "Group":null, "Selected":false, "Text":"    ", "Value":"IsValidLatinStringWithDigits" }, { "Disabled":false, "Group":null, "Selected":false, "Text":"", "Value":"IsValidDigitString" } ], "validationStrategySystemName":"Default", "tooltipMessage":": \"\"", "type":"String" } }, "Enum":{ "name":"", "viewModelType":"Itc.DirectCrm.Web.EnumCustomFieldKindTypedViewModel, itc.DirectCrm.Web, Version=6.459.0.3741, Culture=neutral, PublicKeyToken=null", "componentName":"DirectCrm.EnumCustomFieldKindComponent", "constantComponentData":{ "$type":"Itc.DirectCrm.Web.EnumCustomFieldKindTypedViewModel, itc.DirectCrm.Web, Version=6.459.0.3741, Culture=neutral, PublicKeyToken=null", "selectedEnumValues":null, "forceCreateEnumValue":false, "tooltipMessage":":   - \"ExternalId\",  - \"123\"", "type":"Enum" } } }
      
      







? . , ComponentsMap - :

 public void PrepareForViewing(MvcModelContext mvcModelContext) { ComponentsMap = ModelApplicationHostController .Instance .Get<ReactComponentViewModelConfiguration>() .GetNamedObjectRelatedComponentsMapFor<CustomFieldKindTypedViewModelBase, CustomFieldType>( customFieldViewModel => customFieldViewModel.PrepareForViewing(mvcModelContext)); //  -  }
      
      





, ReactComponentViewModelConfiguration , - CustomFieldKindTypedViewModelBase, . :

 configuration.RegisterNamedObjectRelatedViewModel<CustomFieldKindTypedViewModelBase, CustomFieldType>( () => new StringCustomFieldKindTypedViewModel()); configuration.RegisterNamedObjectRelatedViewModel<CustomFieldKindTypedViewModelBase, CustomFieldType>( () => new IntegerCustomFieldKindTypedViewModel()); configuration.RegisterNamedObjectRelatedViewModel<CustomFieldKindTypedViewModelBase, CustomFieldType>( () => new EnumCustomFieldKindTypedViewModel());
      
      





- , . - C# . , .



検証



:



, , - , . - . , . , , , . , , .

. data-react-validation-summary (. ReactJsFor ). Validation summary — json, - ( ), , -. , validationSummary :





, , .

validation summary :

 { "typedViewModel":{ "selectedEnumValues[0]":{ "systemName":[ "      250 " ] } }, "name":[ " " ] }
      
      





, — , , . ValidationContext, validation summary, :

 interface IValidationContext<TViewModel> { isValid: boolean; getValidationMessageFor: { (propertyExpression: {(model: TViewModel):any}): JSX.Element }; validationMessageExpandedFor: { (propertyExpression: {(model: TViewModel):any}): JSX.Element }; getValidationContextFor: { <TProperty>(propertyExpression: {(model: TViewModel):TProperty}): IValidationContext<TProperty> }; getValidationContextForCollection: { <TProperty>(propertyExpression: {(model: TViewModel):TProperty[]}): {(index: number): IValidationContext<TProperty>} } }
      
      





, . . , «»:

 <FormGroup label="" validationMessage={this.state.validationContext.getValidationMessageFor(m => m.name) }> <Commons.Textbox value={this.state.model.name} width="normal" onChange={getPropertySetter( this.state.model, this._setModel, viewModel => viewModel.name) } /> </FormGroup>
      
      





this.state.validationContext IValidationContext<CustomFieldKindValueBackendViewModel>, . getPropertyNameByPropertyProvider, , getValidationMessageFor validation summary .

, validation summary .

, - , . , , . , — -. - - . , , . , — , , , , , validation summary.

- «» :

 private void RegisterEndUserInput( ISubmodelInputRegistrator<CustomFieldKindValueViewModel> registrator, CustomFieldKind customFieldKind) { //   registrator.RegisterEndUserInput( customFieldKind, cfk => cfk.Name, this, m => m.Name); //   }
      
      





this — -, Name, , Name CustomFieldKind customFieldKind. , Name -.

CustomFieldKind :

 public void Validate(ValidationContext validationContext) { //    validationContext .Validate(this, cfk => cfk.Name) .ToHave(() => !Name.IsNullOrEmpty()) .OrAddError<CustomFieldCustomizationTemplateComponent>(c => c.NameRequired); //    }
      
      





, , , CustomFieldKind.Name , , .



結論として



, UI. , , , :)

, , - , UI , Enterprise. , . ReactJS, - . -, , , , ! .



All Articles