web-dev-qa-db-ja.com

ビューモデルにmvcのドロップダウンの独自のリストを入力させることは有効ですか?

これに関する議論を見つけるのに苦労しました。

ユーザーを更新するための単純なMVCページがあり、その国にドロップダウンリストを配置する場合は、ビューモデルにドロップダウンリストを設定する必要があります。それを_List<Country> countries {get; set;}_と呼びましょう

ページが投稿され、ユーザー名を入力するのを忘れたなどのエラーが発生した場合は、再度ページにリダイレクトして再試行します。この時点で、リダイレクトする前にモデルの_List<Country>_を再入力することも忘れないでください。それらをページに戻します。

私が知っているこれを処理する3つの方法:

  • コントローラで、データベースを呼び出します。 viewModel.Countries = _dataAccess.GetCountries()
  • リストをモデルではなくビューバッグに保存します。 ViewBag.Countries = _dataAccess.GetCountries()
  • それをビューモデル自体に入れて、List<Country> {get { return _dataAccess.GetCountries();DataAccessオブジェクトに注入します。

3番目のアプローチは、コントローラーのフィールドへのデータの入力/再入力について心配する必要がないため、私が検討しているものです。しかし、これが受け入れ可能な方法であることを検索しても証拠を見つけることができないため、考慮していない欠点があるのではないかと心配しています。

3番目のメソッドにもGetCountries()が非同期であるという大きな問題があり、プロパティやコンストラクターで非同期メソッドを呼び出すことはできません。

5
NibblyPig

これがMVCとMVVMのアプローチの主な違いだと思います。

MVCでは、コントローラーに、基本的にビューが必要とするさまざまなデータの単なる構造体であるViewModelを設定します

MVVMにはコントローラーがないため、すべてのロジックはViewModelに入ります。ViewModelは、メソッドとすべてを備えた「適切な」クラスになりました。

ASP MVCフレームワークを使用している場合、ビューはクライアントマシンで実行されているWebページであるという制約があります。viewmodelで生成されたサーバーに実際にコールバックすることはできません。つまり、コントローラーでロジックを実行することもできます。

コントローラがViewModelを構築し、そのメソッドを呼び出して結果を返すようにしても、明らかな害はありません。しかし、それはあなたが必要としない追加のステップです。

たとえば、viewModel.GetCountries()を介して入力された、サーバー側に入力された国のリストを含むページがあるとします。

ただし、国のリストを返すクライアント側のapi呼び出しもあります。

コントローラで行うことを選択しますか

public class UserController
{
    //MVVM
    public Action GetUserFormWithCountryDropDown
    {
        return View(new ViewModel(dataAccess)); //vm has dataAccess.GetCountries in a property
    }

    MVC
    public Action GetUserFormWithCountryDropDown
    {
        var vm= new ViewModel();
        vm.Countries = dataAccess.GetCountries()
        return View(vm);
    }
}

そして

public class CountryApiController
{
    //MVVM
    public List<Country> GetCountryDropDown
    {
        var vm = new ViewModel(dataAccess)
        return vm.Countries() //calls dataaccess
    }
    //MVC
    public List<Country> GetCountryDropDown
    {
        return dataAccess.GetCountries();
    }
}

VMからの単一のプロパティを使用するだけなので、MVVMスタイルを使用することはAPIからは意味がありません。

さらに、クライアント側のコマンドをサーバー側のVMにバインドすることはできません。すべてのHTTP呼び出しでインスタンス化および破棄されます。

MVVMアプローチの欠点は、複雑なビューモデルがあり、その一部のみがアクションを実行したい場合に明らかになります。すなわち。

public class CountryApiController
{
    //MVVM
    public List<Country> GetCountrys
    {
        var dataAccess = new DataAccess(countries);
        var translator = new Translator(htmlContext.Local);
        var facialRecognition = new Recogniser(User.Picture);

        var vm = new UserViewModel(
            dataAccess,
            translator,
            facialRecognition,
            ... etc
            );

        return vm.Countries() //just return a list of countries, other logic not required
        //wait do i need to duplicate this for non user related country lists?
    }

}
3
Ewan

ビューモデルはリクエストごとに再作成されるため、ドロップダウンのオプションを作成する「もの」をビューモデルから分離することをお勧めします。これには、後で自動入力のドロップダウンをさらに簡単に追加できるという追加の利点があります。ここでは、ドロップダウンの新しいオプションをJSON形式で返すコントローラーアクションがあります。

結合を減らすために、ビューモデルからエンティティをできるだけ離し、ビューモデルが各リクエストで作成および破棄される一時的なオブジェクトであることを反映します。オプションの生成に必要な「データアクセス」はエンティティを処理できますが、IEnumerable<SelectListItem>ドロップダウンリストを表すオブジェクトは、エンティティとビューモデル間のこの分離を継続するための良い方法です。

より良い用語がない場合、この新しい「ドロップダウン生成物」をDropDownOptionFactoryと呼ぶことができます。これは、コンストラクター引数としてデータアクセスオブジェクトを受け取るプレーンな古いC#オブジェクトにすることができます。オプションのリストを生成するメソッドがあります。

public class DropDownOptionFactory
{
    private readonly DataAccess dataAccess;

    public DropDownOptionFactory(DataAccess dataAccess)
    {
        this.dataAccess = dataAccess;
    }

    public IEnumerable<SelectListItem> CreateCountryOptions(string defaultItemText = "Select Country", string defaultValue = "")
    {
        var options = new List<SelectListItem>()
        {
            new SelectListItem()
            {
                Text = defaultItemText,
                Value = defaultValue
            }
        };

        options.AddRange(from c in dataAccess.GetCountries()
                         orderby c.Name
                         select new SelectListItem()
                         {
                             Text = c.Name,
                             Value = c.CountryCode
                         });

        return options.AsReadOnly();
    }
}

これで、ビューモデルには、ドロップダウンオプションファクトリのプロパティが必要になります。

public class AddEditUserProfileViewModel
{
    public DropDownOptionFactory DropDowns { get; set; }
    // ... other properties
}

public ActionResult Create()
{
    var model = new AddEditUserProfileViewModel()
    {
        Dropdowns = new DropDownOptionFactory(dataAccess)
    };

    return View(model);
}

ポストバック時にドロップダウンを再設定するには、単一のプロパティを設定する必要があります。

public ActionResult Create(AddEditUserProfileViewModel model)
{
    if (ModelState.IsValid)
    {
        // Save it
        return RedirectToAction("Details", { id = ... });
    }

    // Validations failed, repopulate drop downs
    model.Dropdowns = new DropDownOptionFactory(dataAccess);

    return View(model);
}

リソースを「編集」するときも同じ原則が適用されます。最後に、このドロップダウンロジックが単独で機能する場所があるため、自動入力機能を実装することは、オプションを返す別のコントローラーアクションを作成することを意味します。

public ActionResult CountriesJson()
{
    var dropdowns = new DropDownOptionFactory(dataAccess);

    return JsonOptions(dropdowns.CreateCountryOptions());
}

private ActionResult JsonOptions(IEnumerable<SelectListItem> options)
{
    return Json(options.Select(o => new { text = options.Text, value = options.Value }));
}
1
Greg Burghardt

ページが投稿され、ユーザー名を入力するのを忘れたなどのエラーが発生した場合は、再度ページにリダイレクトして再試行します。その時点で、モデルのリストを再作成してから、ページ。

あなたが言及するように、主な問題はポストメソッドで何かを再計算または補充することです。また、すでにgetメソッドにデータを入力しています。だから、私は「VM自分を繰り返さずに(少ないコード)を埋める方法?」と「誰がVMを埋める責任があるか:それ自体またはコントローラ?」に焦点を当てています。これは強い型ではありません。この場合、VMではなく、VBを好みます。

MVCでは、コントローラーはVMを埋める責任があります。 VM単にデータの種類を考慮します。ヨーロッパの国または世界中の国があることを知ることはできません。したがって、各コントローラはVMしたがって、VM dataaccessを使用してデータを取得することはお勧めしません。VMは、MVCでこれを行う責任はありません。 VM(EuropeanCountriesAsiaCountries ..など)にある種のプロパティを作成します。エンティティレイヤー上のサービスレイヤークラスのように見えますが、それはViewModel( VM)。

コントローラーでプライベートメソッドを作成することをお勧めします。このプライベートメソッドは、getおよびpostメソッドで使用できます。したがって、DRY=を実現し、このメソッドをpostメソッドで呼び出すことができます。また、同じVMを使用する場合、すべてのコントローラーが一種の塗りつぶしメソッドを持つことができます。ヨーロッパの国をVMのCountriesプロパティと別の国は、すべての国をCountriesプロパティに入力できます。

Get、post、1つのプライベートメソッドによるコントローラーでの使用例を以下に示します。

あなたのコントローラーメソッド=>

[HttpGet]
public ActionResult Create()
{
    TaskViewModel viewModel = new TaskViewModel();
    viewModel = initViewModel(viewModel);
    return View(viewModel);
}

[HttpPost]
[ValidateAntiForgeryToken]
public ActionResult Create(TaskViewModel viewModel)
{
    try
    {
        if(ModelState.IsValid)
        {
            // ... to do 
        }
    }
    catch (Exception ex)
    {
        ModelState.AddModelError("", ex.GetProperExceptionMessage());
    }
    // when you do here like that, user country selection and/or other selection(s) will be kept. 'SelectedCountryId'
    viewModel = initViewModel(viewModel);
    return View(viewModel);
}

// by passing view model as parameter, you can keep user selection.
private TaskViewModel initViewModel(TaskViewModel viewModel)
{
    viewModel.Countries = _context.GetCountries();
    //.. others

    return viewModel;
}

あなたのモデル=>

public class TaskViewModel
{
    public IEnumerable<Country> Countries { get; set; }
    public int SelectedCountryId { get; set; }
    public string TaskDescription { get; set; }
}

1
Engineert

これがあなたが求めていることを達成するための私の現在の方法です。 「ページごと」にJUST ONEコントローラーメソッドがあるという点で、私はこれが好きです。

// Here's the ViewModel, you can add validation via IValidatableObject if you like

public class CreateWidgetViewModel 
{
    public int SelectedDangleId { get; set; } // to be collected from user input via DropDownList

    [ReadOnly(true)] // prevents malicious user from posting their own collection thru Model Binder
    public IEnumerable<Dangle> DangleChoices { get; set; }

    public IEnumerable<SelectListItem> GetDangleChoices() =>
        SelectlistHelpers.Generate(AvailableExamTemplates.ToDictionary(k => k.id, v => v.name), SelectedExamTemplateId);
        // thats a helper method of mine that takes a Dict<T1, T2> and turns it into a collection of SelectListTems
}

// The controller action here is just ONE route w/ the method below, so you won't forget to populate the Dropdownlist on a POST.
// Its also Nice that I control my ViewModel constructor, its never envoked through the model binder.

[Route("CreateWidget/{fooId:int}")]
[AcceptVerbs(HttpVerbs.Get | HttpVerbs.Post)]
public ActionResult CreateWidget(int fooId)
{
    var viewModel = new CreateWidgetViewModel();

    if (Request?.HttpMethod == "POST") 
    {
        // This binds the dropdown choice to SelectedDangleId
        // If your ViewModel implements IValidatableObject, it runs here and affects ModelState.IsValid appropriately
        TryUpdateModel(viewModel);  

        if (ModelState.IsValid) 
        {
            // all good? then do something w/ SelectedDangleId, and fooId from URL param, etc
            if (allGood)
                return Redirect($"/Widgets/{whatever}/");
        }
    }

    viewModel.DangleChoices = getDanglesFromDBaseOrMaybeCache();
    return View(vm);
}


// In my view, I bind the Dangle choices to a dropdown via:

@Html.DropDownListFor(model => model.SelectedDangleId, GetDangleChoices())
0
Graham