3

オブジェクトとそのすべての子コレクションを更新するために1つのビューを使用しようとしています(エンティティフレームワークモデルを使用したSQL Serverデータベースの1対多の関係に基づいています)。

AutoMapperを使用するように提案され、それを試して動作させました。(子コレクションを持つモデルにAutoMapperを使用しようとすると、Asp.Net MVC 3でnullエラーが発生するを参照してください)。

しかし、ソリューションを維持するのは本当に難しいです。そして、最初に必要だった単純なものを試してみると、エンティティオブジェクトをモデルとして直接使用して(「コンサルタント」オブジェクト、すべての子コレクションの親)、正しい変更されたすべてのデータを元に戻すことができます。 POST、およびUpdateModelを使用して、子コレクションを含むそれらを取得できます。単純。確かに、UpdateModelは、ここSOのヒントからカスタムモデルバインダーを作成した後にのみ機能しました。

私のカスタムモデルバインダーから:

public override object BindModel(ControllerContext controllerContext, ModelBindingContext bindingContext)
        {
            bindingContext.ModelMetadata.ConvertEmptyStringToNull = false;

            return base.BindModel(controllerContext, bindingContext);
        }

        protected override void SetProperty(ControllerContext controllerContext, ModelBindingContext bindingContext, PropertyDescriptor propertyDescriptor, object value)
        {
            ModelMetadata propertyMetadata = bindingContext.PropertyMetadata[propertyDescriptor.Name];
            propertyMetadata.Model = value;
            string modelStateKey = CreateSubPropertyName(bindingContext.ModelName, propertyMetadata.PropertyName);

            // Try to set a value into the property unless we know it will fail (read-only 
            // properties and null values with non-nullable types)
            if (!propertyDescriptor.IsReadOnly)
            {
                try
                {
                    if (value == null)
                    {
                        propertyDescriptor.SetValue(bindingContext.Model, value);
                    }
                    else
                    {
                        Type valueType = value.GetType();

                        if (valueType.IsGenericType && valueType.GetGenericTypeDefinition() == typeof(EntityCollection<>))
                        {
                            IListSource ls = (IListSource)propertyDescriptor.GetValue(bindingContext.Model);
                            IList list = ls.GetList();

                            foreach (var item in (IEnumerable)value)
                            {
                                list.Add(item);
                            }
                        }
                        else
                        {
                            propertyDescriptor.SetValue(bindingContext.Model, value);
                        }
                    }

                }
                catch (Exception ex)
                {
                    // Only add if we're not already invalid
                    if (bindingContext.ModelState.IsValidField(modelStateKey))
                    {
                        bindingContext.ModelState.AddModelError(modelStateKey, ex);
                    }
                }
            }
        }

これが私の簡単なEditPOSTメソッドです:

    [HttpPost]
    [ValidateInput(false)] //To allow HTML in description box
    public ActionResult Edit(int id, FormCollection collection)
    {

        Consultant consultant = _repository.GetConsultant(id);
        UpdateModel(consultant);
        _repository.Save();

        return RedirectToAction("Index");
    }

しかしその後、UpdateModelは機能しました。問題は、次の段階で、コンテキストでSaveChangesを呼び出そうとすると、失敗することです。このエラーが発生します:

操作が失敗しました:1つ以上の外部キープロパティがnull許容でないため、関係を変更できませんでした。リレーションシップに変更が加えられると、関連する外部キープロパティがnull値に設定されます。外部キーがnull値をサポートしていない場合は、新しい関係を定義するか、外部キープロパティに別の非null値を割り当てるか、関連のないオブジェクトを削除する必要があります。

何が悪いのかわかりません。投稿されたConsultantオブジェクトにすべての正しい値が表示されていますが、データベースに保存できません。この場合のAutoMapperのルート(興味深いツールですが)はうまく機能していませんが、コードが非常に複雑になり、アプリケーションがかなり単純なものになり、維持するのが悪夢になっています。

なぜ私がこのエラーを受け取っているのか、そしてそれを克服する方法について誰かが洞察を提供できますか?

アップデート:

ここでいくつかの投稿を読んで、少し関連しているように見える投稿を見つけました:エンティティフレームワークを使用して、asp.net MVC2からデータベースのモデルを更新する方法は?。これに関連するかどうかはわかりませんが、POST後にConsultantオブジェクトを調べたところ、このオブジェクト自体にentitykeyがあるように見えますが、コレクション内の個々のアイテムにはありません(EntityKeySet = null)。ただし、各アイテムには正しいIDがあります。私はEntityKeyでこれを理解するふりをしていないので、それが私の問題に関係があるかどうか、もしそうなら、それを解決する方法を説明してください...

更新2:

私は自分の問題と関係があるかもしれない何かを考えました:ビューはスティーブンサンダーソンによって説明された技術を使用しています(http://blog.stevensanderson.com/2010/01/28/editing-a-variable-length-を参照) list-aspnet-mvc-2-style /)、デバッグすると、UpdateModelがビュー内のコレクション内のアイテムを実際のConsultantオブジェクト内のアイテムと一致させるのに問題があるように見えます。これがこの手法の索引付けに関係しているのではないかと思います。これがそのコードのヘルパーです(私はそれを自分でうまくフォローすることはできませんが、Guidを使用してインデックスを作成します。これが問題になる可能性があります):

public static class HtmlPrefixScopeExtensions
    {
        private const string idsToReuseKey = "__htmlPrefixScopeExtensions_IdsToReuse_";

        public static IDisposable BeginCollectionItem(this HtmlHelper html, string collectionName)
        {
            var idsToReuse = GetIdsToReuse(html.ViewContext.HttpContext, collectionName);
            string itemIndex = idsToReuse.Count > 0 ? idsToReuse.Dequeue() : Guid.NewGuid().ToString();

            // autocomplete="off" is needed to work around a very annoying Chrome behaviour whereby it reuses old values after the user clicks "Back", which causes the xyz.index and xyz[...] values to get out of sync.
            html.ViewContext.Writer.WriteLine(string.Format("<input type=\"hidden\" name=\"{0}.index\" autocomplete=\"off\" value=\"{1}\" />", collectionName, html.Encode(itemIndex)));

            return BeginHtmlFieldPrefixScope(html, string.Format("{0}[{1}]", collectionName, itemIndex));
        }

        public static IDisposable BeginHtmlFieldPrefixScope(this HtmlHelper html, string htmlFieldPrefix)
        {
            return new HtmlFieldPrefixScope(html.ViewData.TemplateInfo, htmlFieldPrefix);
        }

        private static Queue<string> GetIdsToReuse(HttpContextBase httpContext, string collectionName)
        {
            // We need to use the same sequence of IDs following a server-side validation failure,  
            // otherwise the framework won't render the validation error messages next to each item.
            string key = idsToReuseKey + collectionName;
            var queue = (Queue<string>)httpContext.Items[key];
            if (queue == null)
            {
                httpContext.Items[key] = queue = new Queue<string>();
                var previouslyUsedIds = httpContext.Request[collectionName + ".index"];
                if (!string.IsNullOrEmpty(previouslyUsedIds))
                    foreach (string previouslyUsedId in previouslyUsedIds.Split(','))
                        queue.Enqueue(previouslyUsedId);
            }
            return queue;
        }

        private class HtmlFieldPrefixScope : IDisposable
        {
            private readonly TemplateInfo templateInfo;
            private readonly string previousHtmlFieldPrefix;

            public HtmlFieldPrefixScope(TemplateInfo templateInfo, string htmlFieldPrefix)
            {
                this.templateInfo = templateInfo;

                previousHtmlFieldPrefix = templateInfo.HtmlFieldPrefix;
                templateInfo.HtmlFieldPrefix = htmlFieldPrefix;
            }

            public void Dispose()
            {
                templateInfo.HtmlFieldPrefix = previousHtmlFieldPrefix;
            }
        }
    }

しかし、繰り返しになりますが、非表示の入力にはvalue属性にidが含まれているため、これが問題になるとは思わなかったでしょう。UpdateModelはフィールドの名前を調べて、Programs(コレクション)とName(プロパティ)、次にIDの値...?また、更新中に不一致があるようです。とにかく、FireBugから生成されたhtmlもここにあります:

<td>
            <input type="hidden" value="1" name="Programs[cabac7d3-855f-45d8-81b8-c31fcaa8bd3d].Id" id="Programs_cabac7d3-855f-45d8-81b8-c31fcaa8bd3d__Id" data-val-required="The Id field is required." data-val-number="The field Id must be a number." data-val="true"> 
            <input type="text" value="Visual Studio" name="Programs[cabac7d3-855f-45d8-81b8-c31fcaa8bd3d].Name" id="Programs_cabac7d3-855f-45d8-81b8-c31fcaa8bd3d__Name">
            <span data-valmsg-replace="true" data-valmsg-for="Programs[cabac7d3-855f-45d8-81b8-c31fcaa8bd3d].Name" class="field-validation-valid"></span>
        </td>

これが問題かどうか誰か知っていますか?もしそうなら、UpdateModelでコレクションを簡単に更新できるようにするにはどうすればよいですか?(POSTの前にビューでアイテムを追加または削除することはできますが、これが最初からこの手法の目的でした)。

4

3 に答える 3

1

コンサルタントエンティティと1対多の関係を持つ親エンティティがあるようです。その関係のForeignKeyとして使用されるConsultantエンティティの属性を変更すると、Entity Frameworkは、親エンティティの関連フィールドをnullに設定して、関係を分離します。そのフィールドがnull許容でない場合、このエラーが発生します。実際、そのエラー定義は驚くほど優れています。私はこの問題をはるかに多くの不可解なエラーで見ました。

したがって、データベース内の親エンティティを確認し、そこから解決策に進むことをお勧めします(null許容に変更できる場合は、別の制約の一部である場合はすべて問題ありません-pkなど-オブジェクトモデルをいじる)。エンティティモデルを投稿していただきたいのですが、テキストのチャンクはそのままでは威圧的です。

于 2011-03-03T10:37:59.267 に答える
1

発生しているエラーは次のことに関連していると思います。EF4:コレクションから子オブジェクトを削除しても削除されません-なぜですか?どこかに孤児を作成しました。

于 2011-03-06T16:28:35.083 に答える
1

はい、HtmlPrefixScopeExtensionsに関連していますが、これはMvcFuturesモデルバインダーを使用しているためです。global.asax.csでコメントアウト

Microsoft.Web.Mvc.ModelBinding.ModelBinderConfig.Initialize(); 

そして再試行してください:それは大丈夫です!

この問題は、MVC先物モデルバインダーがこのケースを正しく処理しないために発生します。フォームを送信すると、フォームデータがモデルに変換されますが、HtmlPrefixScopeExtensionsを使用して非インクリメンタルIDを生成すると、ModelStateオブジェクトに入力するときに問題が発生します。

モデル自体は、フォームデータから正しく作成されます。問題は、コレクションのすべての要素ではなく、コレクションの最後の値のみを含むModelState内にあります。

強く型付けされたヘルパーメソッド(リストをレンダリングする)は、Modelプロパティリストと、リストに変換される一致するModelStateエントリにあるアイテムのみを選択します。したがって、一致するModelStateエントリにはアイテムが1つしかないため、他のリストアイテムの選択が解除されます。

このメソッドは、強く型付けされたヘルパーコードによって呼び出されます。

htmlHelper.GetModelStateValue(fullName, typeof(string[]))

ModelState ["Programs [cabac7d3-855f-45d8-81b8-c31fcaa8bd3d] .List"]。Valueにはリストの最後の要素のみが含まれるため、リストの最後の要素のみを返します。

これは、MVC3 Futures拡張可能モデルバインダーのバグ(またはサポートされていないシナリオ)です。

于 2011-04-25T09:23:46.453 に答える