your programing

MVVM을 사용한 적절한 검증

lovepro 2020. 12. 31. 23:06
반응형

MVVM을 사용한 적절한 검증


경고 : 매우 길고 상세한 게시물입니다.

좋아, MVVM을 사용할 때 WPF에서 유효성 검사. 나는 지금 많은 것을 읽고, 많은 질문들을보고, 많은 접근법을 시도했지만 , 모든 것이 어느 시점에서 다소 엉망인 것처럼 느껴지고 나는 그것을 올바른 방법으로 하는 방법을 정말로 잘 모르겠습니다 .

이상적으로는보기 모델에서 모든 유효성 검사가 수행되도록하고 싶습니다 IDataErrorInfo. 그래서 내가 한 일입니다. 그러나이 솔루션이 전체 유효성 검사 항목에 대한 완전한 솔루션이 아닌 다른 측면이 있습니다.

그 상황

다음과 같은 간단한 형식을 취합시다. 보시다시피 화려하지 않습니다. 뷰 모델에서 각각 stringint속성에 바인딩하는 두 개의 텍스트 상자가 있습니다. 또한 우리는 ICommand.

문자열과 정수 입력 만있는 간단한 양식

따라서 유효성 검사를 위해 이제 두 가지 선택 사항이 있습니다.

  1. 텍스트 상자의 값이 변경 될 때마다 자동으로 유효성 검사를 실행할 수 있습니다. 따라서 사용자는 잘못된 것을 입력했을 때 즉각적인 응답을받습니다.
    • 한 단계 더 나아가 오류가있을 때 버튼을 비활성화 할 수 있습니다.
  2. 또는 버튼을 눌렀을 때만 명시 적으로 유효성 검사를 실행 한 다음 해당되는 경우 모든 오류를 표시 할 수 있습니다. 분명히 여기서는 오류 버튼을 비활성화 할 수 없습니다.

이상적으로는 선택 1을 구현하고 싶습니다 ValidatesOnDataErrors. 활성화 된 일반 데이터 바인딩의 경우 이것이 기본 동작입니다. 따라서 텍스트가 변경되면 바인딩이 소스를 업데이트하고 IDataErrorInfo해당 속성에 대한 유효성 검사를 트리거합니다 . 오류는보기에서 다시보고됩니다. 여태까지는 그런대로 잘됐다.

보기 모델의 유효성 검사 상태

흥미로운 점은 뷰 모델 또는이 경우 버튼에 오류가 있는지 알려주는 것입니다. IDataErrorInfo작동 방식 은 주로 뷰에 오류를보고하는 것입니다. 따라서보기는 오류가 있는지 쉽게 확인하고 오류를 표시하고 Validation.Errors. 또한 유효성 검사는 항상 단일 속성에서 발생합니다.

따라서 뷰 모델이 오류가 있는지 또는 유효성 검사가 성공했는지 알 수 있도록하는 것은 까다 롭습니다. 일반적인 해결책은 IDataErrorInfo뷰 모델 자체의 모든 속성에 대한 유효성 검사 를 간단히 트리거하는 것입니다. 이것은 종종 별도의 IsValid속성을 사용하여 수행 됩니다. 이점은 명령을 비활성화하는데도 쉽게 사용할 수 있다는 것입니다. 단점은 모든 속성에 대한 유효성 검사를 너무 자주 실행할 수 있지만 대부분의 유효성 검사는 성능을 손상시키지 않을만큼 충분해야한다는 것입니다. 또 다른 해결책은 유효성 검사를 사용하여 오류를 생성 한 속성을 기억하고 해당 속성 만 확인하는 것이지만 대부분의 경우 약간 복잡하고 불필요 해 보입니다.

결론은 이것이 잘 작동 할 수 있다는 것입니다. IDataErrorInfo모든 속성에 대한 유효성 검사를 제공하며 뷰 모델 자체에서 해당 인터페이스를 사용하여 전체 개체에 대해서도 유효성 검사를 실행할 수 있습니다. 문제 소개 :

바인딩 예외

뷰 모델은 속성에 실제 유형을 사용합니다. 따라서이 예에서 정수 속성은 실제 int. 보기에 사용되는 텍스트 상자는 기본적으로 텍스트 만 지원 합니다 . 따라서 int뷰 모델에서에 바인딩 할 때 데이터 바인딩 엔진은 자동으로 형식 변환을 수행하거나 적어도 시도합니다. 숫자를 의미하는 텍스트 상자에 텍스트를 입력 할 수 있다면 내부에 항상 유효한 숫자가 없을 가능성이 높습니다. 따라서 데이터 바인딩 엔진이 변환에 실패하고 FormatException.

데이터 바인딩 엔진에서 예외가 발생하고 뷰에 표시됩니다.

보기 쪽에서 쉽게 볼 수 있습니다. 바인딩 엔진의 예외는 WPF에 의해 자동으로 포착되고 오류로 표시됩니다 Binding.ValidatesOnExceptions. setter에서 발생한 예외에 필요한 것을 활성화 필요조차 없습니다 . 오류 메시지에는 일반 텍스트가 있으므로 문제가 될 수 있습니다. Binding.UpdateSourceExceptionFilter처리기 를 사용하여 throw되는 예외를 검사하고 소스 속성을 살펴본 다음 대신 덜 일반적인 오류 메시지를 생성 하여이 문제를 해결했습니다 . 모든 것이 내 자신의 Binding 태그 확장으로 캡슐화되었으므로 필요한 모든 기본값을 가질 수 있습니다.

그래서보기는 좋습니다. 사용자는 오류를 만들고 오류 피드백을보고 수정할 수 있습니다. 그러나 뷰 모델 은 손실됩니다 . 바인딩 엔진에서 예외가 발생 했으므로 소스가 업데이트되지 않았습니다. 따라서 뷰 모델은 여전히 ​​사용자에게 표시되는 것이 아닌 이전 값에 있으며 IDataErrorInfo유효성 검사가 분명히 적용되지 않습니다.

더 나쁜 것은 뷰 모델이이를 알 수있는 좋은 방법이 없다는 것입니다. 적어도 이것에 대한 좋은 해결책을 아직 찾지 못했습니다. 가능한 것은보기가 오류가 있었다는보기 모델에 다시보고하도록하는 것입니다. 이는 Validation.HasError속성을 뷰 모델에 다시 바인딩하여 수행 할 수 있으므로 (직접 불가능) 뷰 모델은 먼저 뷰의 상태를 확인할 수 있습니다.

또 다른 옵션은 처리 된 예외 Binding.UpdateSourceExceptionFilter를 뷰 모델 전달하여 이에 대한 알림도받는 것입니다. 뷰 모델은 바인딩이 이러한 사항을보고하는 인터페이스를 제공하여 일반적인 유형별 오류 메시지 대신 사용자 지정 오류 메시지를 허용 할 수도 있습니다. 그러나 그것은 일반적으로 피하고 싶은 뷰에서 뷰 모델로 더 강력한 결합을 생성합니다.

또 다른 "솔루션"은 유형이 지정된 모든 속성을 제거 string하고 일반 속성을 사용 하고 대신 뷰 모델에서 변환을 수행하는 것입니다. 이것은 분명히 모든 유효성 검사를 뷰 모델로 이동하지만 데이터 바인딩 엔진이 일반적으로 처리하는 항목의 엄청난 양의 중복을 의미하기도합니다. 또한 뷰 모델의 의미를 변경합니다. 나에게 뷰는 그 반대가 아닌 뷰 모델 용으로 만들어졌습니다. 물론 뷰 모델의 디자인은 뷰가 수행 할 작업에 따라 달라 지지만 뷰가 그렇게하는 방식에는 여전히 일반적인 자유가 있습니다. 따라서 뷰 모델은 int숫자가 있기 때문에 속성을 정의합니다 . 이제 뷰는 텍스트 상자 (이러한 모든 문제 허용)를 사용하거나 기본적으로 숫자와 함께 작동하는 것을 사용할 수 있습니다. 그래서 아니요, 속성 유형을string 나를위한 옵션이 아닙니다.

결국 이것은 견해의 문제입니다. 뷰 (및 해당 데이터 바인딩 엔진)는 뷰 모델에 작업 할 적절한 값을 제공합니다. 그러나이 경우 뷰 모델에 이전 속성 값을 무효화해야한다고 알려주는 좋은 방법이없는 것 같습니다.

BindingGroups

바인딩 그룹 은이 문제를 해결하기 위해 시도한 한 가지 방법입니다. 바인딩 그룹은 IDataErrorInfo예외를 포함하여 모든 유효성 검사를 그룹화 할 수 있습니다. 뷰 모델에서 사용할 수있는 경우 이러한 모든 유효성 검사 소스에 대한 유효성 검사 상태를 확인하는 수단도 있습니다 ( 예 : CommitEdit.

기본적으로 바인딩 그룹은 위에서 선택 2를 구현합니다. 바인딩을 명시 적으로 업데이트하여 기본적으로 커밋되지 않은 상태를 추가합니다 . 따라서 버튼을 클릭하면 명령이 이러한 변경 사항을 커밋 하고 소스 업데이트 및 모든 유효성 검사를 트리거하며 성공한 경우 단일 결과를 얻을 수 있습니다. 따라서 명령의 동작은 다음과 같습니다.

 if (bindingGroup.CommitEdit())
     SaveEverything();

CommitEdit모든 유효성 검사가 성공한 경우에만 true를 반환합니다 . 그것은 걸릴 IDataErrorInfo계정으로도 바인딩 예외를 확인합니다. 이것은 선택 2에 대한 완벽한 솔루션 인 것 같습니다. 약간의 번거 로움은 바인딩으로 바인딩 그룹을 관리하는 것뿐입니다. 그러나 저는 주로 이것을 처리하는 무언가를 직접 만들었습니다 ( 관련 ).

바인딩에 바인딩 그룹이있는 경우 바인딩은 기본적으로 명시 적 UpdateSourceTrigger. 바인딩 그룹을 사용하여 위의 선택 1을 구현하려면 기본적으로 트리거를 변경해야합니다. 어쨌든 사용자 정의 바인딩 확장이 있기 때문에 이것은 다소 간단 LostFocus합니다.

따라서 이제 바인딩은 텍스트 필드가 변경 될 때마다 계속 업데이트됩니다. 소스를 업데이트 할 수있는 경우 (바인딩 엔진에서 예외가 발생하지 않음) IDataErrorInfo평소대로 실행됩니다. 업데이트 할 수없는 경우에도보기에서 볼 수 있습니다. 버튼을 클릭하면 기본 명령이 호출 CommitEdit(커밋 할 필요가 없음)하고 전체 유효성 검사 결과를 가져와 계속할 수 있는지 확인할 수 있습니다.

이렇게하면 버튼을 쉽게 비활성화 할 수 없습니다. 적어도 뷰 모델에서는 아닙니다. 유효성 검사를 계속해서 확인하는 것은 명령 상태를 업데이트하는 것만으로는 좋은 생각이 아니며, 어쨌든 바인딩 엔진 예외가 발생했을 때 (버튼을 비활성화해야 함)보기 모델은 알림을받지 않습니다. 버튼을 다시 활성화하십시오. 를 사용하여 뷰에서 버튼을 비활성화하는 트리거를 추가 할 수 Validation.HasError있으므로 불가능하지 않습니다.

해결책?

그래서 전반적으로 이것은 완벽한 해결책 인 것 같습니다. 그래도 내 문제는 무엇입니까? 솔직히 잘 모르겠습니다. 바인딩 그룹은 일반적으로 더 작은 그룹에서 사용되는 것처럼 보이는 복잡한 항목으로, 단일보기에 여러 바인딩 그룹이있을 수 있습니다. 내 유효성을 확인하기 위해 전체 뷰에 하나의 큰 바인딩 그룹을 사용하면 마치 악용하는 것처럼 느껴집니다. 그리고 저는이 모든 상황을 해결하는 더 나은 방법이 있어야한다고 계속 생각합니다. 왜냐하면이 문제를 가진 유일한 사람은 저만있을 수 없기 때문입니다. 그리고 지금까지 많은 사람들이 MVVM으로 유효성 검사를 위해 바인딩 그룹을 사용하는 것을 전혀 보지 못했기 때문에 이상하게 느껴집니다.

그렇다면 바인딩 엔진 예외를 확인할 수있는 동안 MVVM을 사용하여 WPF에서 유효성 검사를 수행하는 올바른 방법은 정확히 무엇입니까?


내 솔루션 (/ 해킹)

우선, 귀하의 의견에 감사드립니다! 위에서 언급했듯이 IDataErrorInfo이미 데이터 유효성 검사를 수행하는 데 사용 하고 있으며 개인적으로 유효성 검사 작업을 수행하는 가장 편리한 유틸리티라고 생각합니다. Sheridan이 아래 답변에서 제안한 것과 유사한 유틸리티를 사용하고 있으므로 유지 관리도 잘 작동합니다.

결국 내 문제는 바인딩 예외 문제로 귀결되었습니다. 여기서 뷰 모델은 언제 발생했는지 알 수 없습니다. 위에서 설명한대로 바인딩 그룹으로이 문제를 처리 할 수 ​​있었지만 그다지 편안하지 않았기 때문에 여전히 반대하기로 결정했습니다. 그래서 대신 무엇을 했습니까?

위에서 언급했듯이 바인딩의 .NET Framework를 수신하여 뷰 측에서 바인딩 예외를 감지 UpdateSourceExceptionFilter합니다. 거기에서 바인딩 표현식의 .NET Framework에서 뷰 모델에 대한 참조를 얻을 수 있습니다 DataItem. 그런 다음 IReceivesBindingErrorInformation바인딩 오류에 대한 정보에 대한 가능한 수신기로 뷰 모델을 등록 하는 인터페이스 가 있습니다. 그런 다음이를 사용하여 바인딩 경로와 예외를 뷰 모델에 전달합니다.

object OnUpdateSourceExceptionFilter(object bindExpression, Exception exception)
{
    BindingExpression expr = (bindExpression as BindingExpression);
    if (expr.DataItem is IReceivesBindingErrorInformation)
    {
        ((IReceivesBindingErrorInformation)expr.DataItem).ReceiveBindingErrorInformation(expr.ParentBinding.Path.Path, exception);
    }

    // check for FormatException and produce a nicer error
    // ...
 }

그런 다음 뷰 모델에서 경로의 바인딩 표현식에 대한 알림을받을 때마다 기억합니다.

HashSet<string> bindingErrors = new HashSet<string>();
void IReceivesBindingErrorInformation.ReceiveBindingErrorInformation(string path, Exception exception)
{
    bindingErrors.Add(path);
}

그리고 IDataErrorInfo속성 유효성을 다시 검사 할 때마다 바인딩이 작동했음을 알고 해시 세트에서 속성을 지울 수 있습니다.

그런 다음 뷰 모델에서 해시 세트에 항목이 포함되어 있는지 확인하고 데이터를 완전히 검증해야하는 작업을 중단 할 수 있습니다. 뷰에서 뷰 모델로의 결합으로 인해 가장 좋은 솔루션이 아닐 수도 있지만 해당 인터페이스를 사용하는 것은 적어도 다소 문제가 적습니다.


경고 : 긴 답변도

IDataErrorInfo유효성 검사를 위해 인터페이스를 사용 하지만 필요에 맞게 사용자 정의했습니다. 나는 그것이 당신의 문제 중 일부도 해결한다는 것을 알게 될 것이라고 생각합니다. 귀하의 질문에 대한 한 가지 차이점은 기본 데이터 유형 클래스에서 구현한다는 것입니다.

지적했듯이이 인터페이스는 한 번에 하나의 속성 만 처리하지만 오늘날에는 분명히 좋지 않습니다. 그래서 대신 사용할 컬렉션 속성을 추가했습니다.

protected ObservableCollection<string> errors = new ObservableCollection<string>();

public virtual ObservableCollection<string> Errors
{
    get { return errors; }
}

외부 오류를 표시 할 수없는 문제를 해결하기 위해 (귀하의 경우보기에서, 내 경우에는보기 모델에서) 간단히 다른 컬렉션 속성을 추가했습니다.

protected ObservableCollection<string> externalErrors = new ObservableCollection<string>();

public ObservableCollection<string> ExternalErrors
{
    get { return externalErrors; }
}

나는이 HasError내 컬렉션에 보이는 속성을 :

public virtual bool HasError
{
    get { return Errors != null && Errors.Count > 0; }
}

이것은 예를 들어 Grid.Visibility사용자 정의 사용하여 이것을 바인딩 할 수 있습니다 BoolToVisibilityConverter. 표시하는 Grid어떤 오류가있는 것을 알 안에 수집 제어. 또한 오류를 강조 표시 Brush하기 Red위해 a 변경할 수 Converter있지만 (또 다른 사용 ) 아이디어를 얻은 것 같습니다.

그런 다음 각 데이터 유형 또는 모델 클래스에서 Errors속성을 재정의 하고 Item인덱서를 구현합니다 (이 예제에서는 단순화 됨).

public override ObservableCollection<string> Errors
{
    get
    {
        errors = new ObservableCollection<string>();
        errors.AddUniqueIfNotEmpty(this["Name"]);
        errors.AddUniqueIfNotEmpty(this["EmailAddresses"]);
        errors.AddUniqueIfNotEmpty(this["SomeOtherProperty"]);
        errors.AddRange(ExternalErrors);
        return errors;
    }
}

public override string this[string propertyName]
{
    get
    {
        string error = string.Empty;
        if (propertyName == "Name" && Name.IsNullOrEmpty()) error = "You must enter the Name field.";
        else if (propertyName == "EmailAddresses" && EmailAddresses.Count == 0) error = "You must enter at least one e-mail address into the Email address(es) field.";
        else if (propertyName == "SomeOtherProperty" && SomeOtherProperty.IsNullOrEmpty()) error = "You must enter the SomeOtherProperty field.";
        return error;
    }
}

AddUniqueIfNotEmpty방법은 사용자 정의 extension방법이며 '주석에 표시된대로 수행'합니다. 차례로 유효성을 검사하려는 각 속성을 호출하고 중복 오류를 무시하고 컬렉션을 컴파일하는 방법에 유의하십시오.

ExternalErrors컬렉션을 사용하여 데이터 클래스에서 확인할 수없는 항목을 확인할 수 있습니다.

private void ValidateUniqueName(Genre genre)
{
    string errorMessage = "The genre name must be unique";
    if (!IsGenreNameUnique(genre))
    {
        if (!genre.ExternalErrors.Contains(errorMessage)) genre.ExternalErrors.Add(errorMessage);
    }
    else genre.ExternalErrors.Remove(errorMessage);
}

사용자가 int필드에 알파벳 문자를 입력하는 상황에 대한 귀하의 요점을 해결하기 IsNumeric AttachedProperty위해 TextBox, 예를 들어 사용자 정의를 사용하는 경향이 있습니다 . 나는 그들이 이런 종류의 오류를 범하게하지 않는다. 나는 항상 그것을 멈추고 그것을 고치는 것보다 그것을 멈추는 것이 낫다고 느낍니다.

전반적으로 WPF의 유효성 검사 기능에 정말 만족하고 전혀 원하지 않습니다.

마무리하고 완성도를 높이기 INotifyDataErrorInfo위해이 추가 된 기능 중 일부를 포함 하는 인터페이스 가 이제 있다는 사실을 알려야한다고 느꼈습니다 . MSDN INotifyDataErrorInfo인터페이스 페이지에서 자세한 내용을 확인할 수 있습니다 .


업데이트 >>>

예, ExternalErrors속성은 해당 개체 외부의 데이터 개체와 관련된 오류를 추가하겠습니다. 죄송합니다. 제 예제는 완료되지 않았습니다. IsGenreNameUnique방법을 보여 주었으면 사용하는 것을 보았을 것입니다. LinQ모두Genre컬렉션의 데이터 항목 객체의 이름이 고유 여부를 결정합니다 :

private bool IsGenreNameUnique(Genre genre)
{
    return Genres.Where(d => d.Name != string.Empty && d.Name == genre.Name).Count() == 1;
}

귀하의 int/ string문제에 관해서는 , 데이터 클래스에서 이러한 오류가 발생 하는 것을 볼 수있는 유일한 방법 은 모든 속성을으로 선언하는 것입니다 object.하지만 할 일이 엄청나게 많습니다. 아마도 다음과 같이 속성을 두 배로 늘릴 수 있습니다.

public object FooObject { get; set; } // Implement INotifyPropertyChanged

public int Foo
{
    get { return FooObject.GetType() == typeof(int) ? int.Parse(FooObject) : -1; }
}

그런 경우 Foo코드에서 사용하고 FooObject에 사용 된 Binding, 당신이 할 수 있습니다 :

public override string this[string propertyName]
{
    get
    {
        string error = string.Empty;
        if (propertyName == "FooObject" && FooObject.GetType() != typeof(int)) 
            error = "Please enter a whole number for the Foo field.";
        ...
        return error;
    }
}

이렇게하면 요구 사항을 충족 할 수 있지만 추가 할 코드가 많이 있습니다.


단점은 모든 속성에 대한 유효성 검사를 너무 자주 실행할 수 있지만 대부분의 유효성 검사는 성능을 손상시키지 않을만큼 충분해야한다는 것입니다. 또 다른 해결책은 유효성 검사를 사용하여 오류를 생성 한 속성을 기억하고 해당 속성 만 확인하는 것입니다.하지만 대부분의 경우 약간 복잡하고 불필요 해 보입니다.

어떤 속성에 오류가 있는지 추적 할 필요가 없습니다. 오류가 존재한다는 것만 알면됩니다. 뷰 모델은 오류 목록 (오류 요약 표시에도 유용함)을 유지할 IsValid수 있으며 속성은 목록에 항목이 있는지 여부를 반영 할 수 있습니다. IsValid오류 요약이 최신 상태이고 IsValid변경 될 때마다 새로 고쳐지 는 경우 호출 될 때마다 모든 것을 확인할 필요가 없습니다 .


결국 이것은 견해의 문제입니다. 뷰 (및 해당 데이터 바인딩 엔진)는 뷰 모델에 작업 할 적절한 값을 제공합니다. 그러나이 경우 뷰 모델에 이전 속성 값을 무효화해야한다고 알려주는 좋은 방법이없는 것 같습니다.

뷰 모델에 바인딩 된 컨테이너 내에서 오류를 수신 할 수 있습니다.

container.AddHandler(Validation.ErrorEvent, Container_Error);

...

void Container_Error(object sender, ValidationErrorEventArgs e) {
    ...
}

오류가 추가되거나 제거 될 때이를 알리고 e.Error.Exception존재 여부에 따라 바인딩 예외를 식별 할 수 있으므로보기가 바인딩 예외 목록을 유지하고이를보기 모델에 알릴 수 있습니다.

그러나이 문제에 대한 해결책은 항상 해킹이 될 것입니다. 왜냐하면 뷰가 그 역할을 적절하게 채우지 않아서 사용자에게 뷰 모델 구조를 읽고 업데이트하는 수단을 제공하기 때문입니다. 이것은 사용자 에게 텍스트 상자 대신 " 정수 상자" 를 올바르게 표시 할 때까지 임시 해결책으로 간주되어야합니다 .


제 생각에는 문제는 너무 많은 곳에서 발생하는 검증에 있습니다. 나는 또한 모든 유효성 검사 로그인을 작성하고 싶었지만 ViewModel모든 번호 바인딩이 나를 ViewModel미치게 만들었습니다.

실패하지 않는 바인딩을 만들어이 문제를 해결했습니다. 분명히 바인딩이 항상 성공하면 형식 자체가 오류 조건을 적절하게 처리해야합니다.

실패 가능한 값 유형

실패한 변환을 정상적으로 지원하는 일반 유형을 만드는 것으로 시작했습니다.

public struct Failable<T>
{
    public T Value { get; private set; }
    public string Text { get; private set; }
    public bool IsValid { get; private set; }

    public Failable(T value)
    {
        Value = value;

        try
        {
            var converter = TypeDescriptor.GetConverter(typeof(T));
            Text = converter.ConvertToString(value);
            IsValid = true;
        }
        catch
        {
            Text = String.Empty;
            IsValid = false;
        }
    }

    public Failable(string text)
    {
        Text = text;

        try
        {
            var converter = TypeDescriptor.GetConverter(typeof(T));
            Value = (T)converter.ConvertFromString(text);
            IsValid = true;
        }
        catch
        {
            Value = default(T);
            IsValid = false;
        }
    }
}

잘못된 입력 문자열 (두 번째 생성자)로 인해 형식이 초기화되지 않더라도 잘못된 텍스트 와 함께 잘못된 상태를 조용히 저장합니다 . 이것은 잘못된 입력의 경우에도 바인딩의 왕복을 지원하기 위해 필요합니다 .

일반 값 변환기

위의 유형을 사용하여 일반 값 변환기를 작성할 수 있습니다.

public class StringToFailableConverter<T> : IValueConverter
{
    public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
    {
        if (value.GetType() != typeof(Failable<T>))
            throw new InvalidOperationException("Invalid value type.");

        if (targetType != typeof(string))
            throw new InvalidOperationException("Invalid target type.");

        var rawValue = (Failable<T>)value;
        return rawValue.Text;
    }

    public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
    {
        if (value.GetType() != typeof(string))
            throw new InvalidOperationException("Invalid value type.");

        if (targetType != typeof(Failable<T>))
            throw new InvalidOperationException("Invalid target type.");

        return new Failable<T>(value as string);
    }
}

XAML 편리한 변환기

제네릭 인스턴스를 만들고 사용하는 것은 XAML에서 고통스럽기 때문에 일반적인 변환기의 정적 인스턴스를 만들 수 있습니다.

public static class Failable
{
    public static StringToFailableConverter<Int32> Int32Converter { get; private set; }
    public static StringToFailableConverter<double> DoubleConverter { get; private set; }

    static Failable()
    {
        Int32Converter = new StringToFailableConverter<Int32>();
        DoubleConverter = new StringToFailableConverter<Double>();
    }
}

다른 값 유형은 쉽게 확장 할 수 있습니다.

용법

사용법은 매우 간단합니다. 유형을 다음과 같이 변경하면 int됩니다 Failable<int>.

ViewModel

public Failable<int> NumberValue
{
    //Custom logic along with validation
    //using IsValid property
}

XAML

<TextBox Text="{Binding NumberValue,Converter={x:Static local:Failable.Int32Converter}}"/>

이렇게 하면 속성 을 확인하여 동일한 유효성 검사 메커니즘 ( IDataErrorInfo또는 INotifyDataErrorInfo기타)을 사용할 수 있습니다 . 경우 사실, 당신이 직접 사용할 수 있습니다 .ViewModelIsValidIsValidValue


많은 추가 코드를 구현하지 않으려는 경우 작업을 단순화하려는 노력이 있습니다.

시나리오는 viewmodel에 int 속성 (십진수 또는 다른 비 문자열 유형일 수 있음)이 있고보기에서 텍스트 상자를 바인딩하는 것입니다.

속성의 setter에서 발생하는 뷰 모델에 유효성 검사가 있습니다.

보기에서 사용자는 123abc를 입력하고보기 논리는보기에서 오류를 강조 표시하지만 값이 잘못된 유형이므로 속성을 설정할 수 없습니다. setter는 호출되지 않습니다.

가장 간단한 해결책은 viewmodel의 int 속성을 문자열 속성으로 변경하고 모델에서 값을 캐스트하는 것입니다. 이렇게하면 잘못된 텍스트가 속성의 setter에 도달 할 수 있으며 유효성 검사 코드는 데이터를 확인하고 적절하게 거부 할 수 있습니다.

사람들이 이전에 주어진 문제를 해결하려고 시도한 정교한 (그리고 독창적 인) 방법에서 볼 수 있듯이 WPF의 IMHO 유효성 검사가 손상되었습니다. 저에게는 엄청난 양의 추가 코드를 추가하거나 텍스트 상자가 유효성을 검사 할 수 있도록 자체 유형 클래스를 구현하고 싶지 않으므로 이러한 속성을 문자열에 기반으로하는 것이 약간의 느낌이 들더라도 함께 살 수있는 것입니다. 클러 지.

Microsoft는 int 또는 decimal 속성에 바인딩 된 텍스트 상자의 잘못된 사용자 입력 시나리오가이 사실을 뷰 모델에 우아하게 전달할 수 있도록이를 수정해야합니다. 예를 들어 XAML 컨트롤에 대한 새 바인딩 된 속성을 만들어 뷰 논리 유효성 검사 오류를 뷰 모델의 속성에 전달할 수 있어야합니다.

이 주제에 대한 자세한 답변을 제공해 주신 다른 분들께 감사드립니다.


좋아, 나는 당신이 찾고 있던 답을 찾았다 고 생각합니다 ...
설명하기 쉽지 않을 것입니다-하지만 ..
일단 설명을하면 이해하기 아주 쉽습니다 ...
본 MVVM에 대해 가장 정확 / "인증"된 것 같아요 "표준"또는 최소한 시도 된 표준으로.

하지만 시작하기 전에 .. MVVM에 대해 익숙해 진 개념을 변경해야합니다.

"게다가 뷰 모델의 의미를 변경합니다. 저에게 뷰는 그 반대가 아닌 뷰 모델 용으로 만들어졌습니다. 물론 뷰 모델의 디자인은 뷰가 수행 할 작업에 따라 달라 지지만 여전히 일반적입니다. 보기가 그렇게하는 방식의 자유 "

그 문단이 문제의 원인입니다 ..-왜?

당신은 View-Model이 그 자체로 View에 적응할 역할이 없다고 말하고 있기 때문에 ..
그것은 여러면에서 잘못된 것입니다.

다음과 같은 재산이있는 경우 :

public Visibility MyPresenter { get...

Visibility보기를 제공하는 것이 아니라면 무엇입니까 ?
유형 자체와 속성에 부여 될 이름은 확실히보기를 위해 구성됩니다.

내 경험에 따르면 MVVM에는 두 가지 구별 가능한 뷰 모델 범주가 있습니다.

  • 발표자보기 모델-버튼, 메뉴, 탭 항목 등에 연결됩니다 ....
  • 엔터티보기 모델-엔터티 데이터를 화면으로 가져 오는 컨트롤에 호킹됩니다.

이것들은 완전히 다른 두 가지 문제입니다.

그리고 이제 해결책으로 :

public abstract class ViewModelBase : INotifyPropertyChanged
{
   public event PropertyChangedEventHandler PropertyChanged;

   public void RaisePropertyChanged([CallerMemberName] string propertyName = null)
   {
      if (PropertyChanged != null)
         PropertyChanged(this, new PropertyChangedEventArgs(propertyName));
   }
}


public class VmSomeEntity : ViewModelBase, INotifyDataErrorInfo
{
    //This one is part of INotifyDataErrorInfo interface which I will not use,
    //perhaps in more complicated scenarios it could be used to let some other VM know validation changed.
    public event EventHandler<DataErrorsChangedEventArgs> ErrorsChanged; 

    //will hold the errors found in validation.
    public Dictionary<string, string> ValidationErrors = new Dictionary<string, string>();

    //the actual value - notice it is 'int' and not 'string'..
    private int storageCapacityInBytes;

    //this is just to keep things sane - otherwise the view will not be able to send whatever the user throw at it.
    //we want to consume what the user throw at us and validate it - right? :)
    private string storageCapacityInBytesWrapper;

    //This is a property to be served by the View.. important to understand the tactic used inside!
    public string StorageCapacityInBytes
    {
       get { return storageCapacityInBytesWrapper ?? storageCapacityInBytes.ToString(); }
       set
       {
          int result;
          var isValid = int.TryParse(value, out result);
          if (isValid)
          {
             storageCapacityInBytes = result;
             storageCapacityInBytesWrapper = null;
             RaisePropertyChanged();
          }
          else
             storageCapacityInBytesWrapper = value;         

          HandleValidationError(isValid, "StorageCapacityInBytes", "Not a number.");
       }
    }

    //Manager for the dictionary
    private void HandleValidationError(bool isValid, string propertyName, string validationErrorDescription)
    {
        if (!string.IsNullOrEmpty(propertyName))
        {
            if (isValid)
            {
                if (ValidationErrors.ContainsKey(propertyName))
                    ValidationErrors.Remove(propertyName);
            }
            else
            {
                if (!ValidationErrors.ContainsKey(propertyName))
                    ValidationErrors.Add(propertyName, validationErrorDescription);
                else
                    ValidationErrors[propertyName] = validationErrorDescription;
            }
        }
    }

    // this is another part of the interface - will be called automatically
    public IEnumerable GetErrors(string propertyName)
    {
        return ValidationErrors.ContainsKey(propertyName)
            ? ValidationErrors[propertyName]
            : null;
    }

    // same here, another part of the interface - will be called automatically
    public bool HasErrors
    {
        get
        {
            return ValidationErrors.Count > 0;
        }
    }
}

이제 코드의 어딘가에-버튼 명령 'CanExecute'메서드가 구현에 VmEntity.HasErrors에 대한 호출을 추가 할 수 있습니다.

그리고 지금부터 유효성 검사에 관한 귀하의 코드에 평화가있을 수 있습니다. :)

참조 URL : https://stackoverflow.com/questions/19498485/proper-validation-with-mvvm

반응형