WPF MVVM: Как настроить привязку на пользовательских элементах управления? - PullRequest
0 голосов
/ 02 декабря 2019

У меня проблема с пониманием того, как работает привязка к пользовательским элементам управления и почему она работает не так, как на страницах. То, что я пытаюсь сделать, - это создать отображение ошибки (Имя ошибки, Описание и Советы, в котором рассказывается, как ее устранить), которое будет отображаться в элементе управления контентом, если есть ошибка, или другое, если ошибки нет.

Я делаю это с помощью пользовательского элемента управления, который, по сути, является вложенным представлением на странице, чтобы избежать грубых всплывающих окон, и будет повторно использоваться на нескольких страницах. У меня работает привязка управления контентом, поэтому мы отображаем пользовательский элемент управления, но никакой информации.

Для целей «СУХОЙ» я создал модель ошибок с требуемыми свойствами, а затем использую класс для реализации этого. Модель как список ошибок. В конструкторе я просто добавляю новые ошибки в список ... таким образом, все ошибки приложения находятся в одном месте для простоты обслуживания.

Класс системных ошибок:

public List<ErrorMessageModel> errors;

/// <summary>
/// Constructor creates list with all errors in the program
/// </summary>
public SystemErrors()
{

    errors = new List<ErrorMessageModel>()
    {
        //*** No Error ***/
        new ErrorMessageModel(ErrorCodes.noError, "", "", ""),

        /*** No Devices Found Error ***/
        new ErrorMessageModel(ErrorCodes.noDevicesConnected,
                              "No Devices Found",
                              "We couldn't find any attached USB devices.",
                              "This error occurs when there's no connection between the device and the computer ")

        /*** Next Error ***/
    };
}

private ErrorMessageModel _activeError;
public ErrorMessageModel ActiveError
{
    get { return _activeError; }
    set
    {
        if (value == _activeError)
            return;

        _activeError = value;
        RaisePropertyChanged();
    }
}

public void SetActiveError (byte index)
{
    // Changed to ActiveError = after Mark's answer. No effect.
    _activeError = errors[index];

}

В модели представления страницы мы используем enum ErrorCodes, чтобы имя указывало на индекс ошибки. Поэтому, когда у нас возникает ошибка, мы передаем errorCode методу, который преобразует его в байт, а затем вызывает SetActiveError (byte errorCodeToIndex).

Page ViewModel:

...
private void parseErrorCode(ErrorCodes error)
{
    // Convert Error Code into Index number
    var errorCodeToIndex = (byte)error;

    // Create new error list and populate list
    SystemErrors errors = new SystemErrors();

    errors.SetActiveError(errorCodeToIndex);
}

Теперь идеяздесь нужно установить контекст данных пользовательского элемента управления на SystemError и, таким образом, связать с ActiveError (ActiveError.ErrorName, ActiveError.ErrorDescription и т. д.). Я думал, что это позволит нам использовать один текстовый текст, потому что независимо от того, на какой странице мы находимся, когда у нас возникает ошибка, информация об ошибке всегда поступает из SystemErrors.

Контроль пользователя:

<UserControl x:Class="FirmwareUpdaterUI.Views.ConnectionErrorView"
             xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
             xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
             xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" 
             xmlns:d="http://schemas.microsoft.com/expression/blend/2008" 
             xmlns:local="clr-namespace:FirmwareUpdaterUI.Views"
             xmlns:vm="clr-namespace:FirmwareUpdaterUI.ViewModels"
             xmlns:e="clr-namespace:FirmwareUpdaterUI.Errors"
             mc:Ignorable="d" 
             d:DesignHeight="250" d:DesignWidth="400" BorderBrush="Red" BorderThickness="1px">

    <UserControl.DataContext>
        <e:SystemErrors/>
    </UserControl.DataContext>

    <Grid x:Name="ConnectionErrorView" Visibility="Visible">
            <Grid.RowDefinitions>
                <RowDefinition Height=".5*"/>
                <RowDefinition Height="auto"/>
                <RowDefinition Height="auto"/>
                <RowDefinition Height="auto"/>
                <RowDefinition Height="6*"/>
            </Grid.RowDefinitions>
            <Grid.ColumnDefinitions>
                <ColumnDefinition Width="1.5*"/>
                <ColumnDefinition Width=".5*"/>
                <ColumnDefinition Width="10*"/>
                <ColumnDefinition Width="1*"/>
            </Grid.ColumnDefinitions>

            <!-- Row 1-->
            <StackPanel Grid.Row="1" Grid.Column="2" Orientation="Horizontal">
                <TextBlock>
                    Error:
                </TextBlock>
                <TextBlock Text="{Binding ActiveError.ErrorName, 
                           RelativeSource={RelativeSource AncestorType={x:Type e:SystemErrors}}}"/>
            </StackPanel>

            <!-- Row 2 -->
            <TextBlock Grid.Row="2" Grid.Column="2" Grid.ColumnSpan="2"
                       Text="{Binding ErrorDescription}"/>

            <!-- Row 3 -->
            <TextBlock Grid.Row="3" Grid.Column="2" Grid.RowSpan="2" Grid.ColumnSpan="2" 
                       Text="{Binding Path=ActiveError.ErrorTips, StringFormat=Tips: {0}}" />
        </Grid>

</UserControl>

Но я не могу заставить его работать. Вы можете увидеть все мои оставшиеся неудачные подходы в XAML, но это только царапает поверхность того, что я пробовал. Я могу заставить это работать, если я срежу кишки UC и вставлю его в страницу, так что то, что говорит мне, что привязка к странице имеет иной механизм, чем для пользовательского элемента управления.

I 'мы прочитали кучу уроков, посмотрели несколько видео, но все они как бы пропускают то, как это работает;всегда «чтобы это работало, нам нужен уже работающий код», который помогает, только если у вас точно такая же проблема. Я видел свойства зависимостей, то, что кажется нормальным связыванием, относительным источником к себе, относительным источником к предку и т. Д.

Вопросы:

Так почему пользовательский элемент управления имеетотличается механизм связывания от окон / страниц (почему контекст данных не работает так, как в других местах)? Если нам нужны свойства зависимостей, то почему они не нужны для привязки к страницам? А также в отношении DP, если необходимо, в этом случае, я бы просто сделал ActiveErrorProperty типа ErrorModel, или нам нужен один для каждого подчиненного свойства (ErrorName типа string)? Как связать DP со свойством, с которым мы хотим связать?

Обновление:

Сегодня весь день пытался заставить это работать, поэтому я начал отслеживать и выводить на консоль. Обе ошибки не были связаны, и если бы я вставил Trace.WriteLine в публичном объявлении ActiveError после RaisePC(), ActiveError будет установлено на правильную ошибку. Затем я попытался отследить привязку в XAML, и есть некоторые интересные вещи:

ErrorName(_activeError)= No Devices Found
ErrorName(ActiveError)= No Devices Found
System.Windows.Data Warning: 56 : Created BindingExpression (hash=62991470) for Binding (hash=23560597)
System.Windows.Data Warning: 58 :  Path: 'ActiveError.ErrorName'
System.Windows.Data Warning: 60 : BindingExpression (hash=62991470): Default mode resolved to OneWay
System.Windows.Data Warning: 62 : BindingExpression (hash=62991470): Attach to System.Windows.Controls.TextBlock.Text (hash=2617844)
System.Windows.Data Warning: 67 : BindingExpression (hash=62991470): Resolving source 
System.Windows.Data Warning: 70 : BindingExpression (hash=62991470): Found data context element: TextBlock (hash=2617844) (OK)
System.Windows.Data Warning: 78 : BindingExpression (hash=62991470): Activate with root item SystemErrors (hash=52209455)
System.Windows.Data Warning: 108 : BindingExpression (hash=62991470):   At level 0 - for SystemErrors.ActiveError found accessor RuntimePropertyInfo(ActiveError)
System.Windows.Data Warning: 104 : BindingExpression (hash=62991470): Replace item at level 0 with SystemErrors (hash=52209455), using accessor RuntimePropertyInfo(ActiveError)
System.Windows.Data Warning: 101 : BindingExpression (hash=62991470): GetValue at level 0 from SystemErrors (hash=52209455) using RuntimePropertyInfo(ActiveError): <null>
System.Windows.Data Warning: 106 : BindingExpression (hash=62991470):   Item at level 1 is null - no accessor
System.Windows.Data Warning: 80 : BindingExpression (hash=62991470): TransferValue - got raw value {DependencyProperty.UnsetValue}
System.Windows.Data Warning: 88 : BindingExpression (hash=62991470): TransferValue - using fallback/default value ''
System.Windows.Data Warning: 89 : BindingExpression (hash=62991470): TransferValue - using final value ''

Обратите внимание, что это показывает, что ActiveError установлен правильно (первые две строки, «Устройства не найдены» - это ErrorName) до мы видим сбой привязки. Я слишком новичок в WPF, но если я правильно интерпретирую трассировку, похоже, она находит ActiveError в текстовом тексте SystemErrors, но не может получить что-либо из ActiveError.ErrorName, которое, как мы знаем, установлено на правильное значение. О чем это?

Ответы [ 3 ]

2 голосов
/ 02 декабря 2019

SystemErrors не является визуальным предком UserControl. Это DataContext, поэтому следующее должно работать в отношении привязки при условии, что класс ErrorMessageModel имеет открытое свойство ErrorName, которое возвращает то, что вы ожидаете получить:

<TextBlock Text="{Binding ActiveError.ErrorName}"/>

Однако следующее не установит свойство ErrorMessageModel и вызовет событие PropertyChanged:

_activeError = errors[index];

Необходимо установить для свойства новое ErrorMessageModel object:

public void SetActiveError(byte index)
{
    ActiveError = errors[index];
}

Также убедитесь, что вы вызываете метод SetActiveError для фактического экземпляра класса SystemErrors, который вы создаете в разметке XAML:

<UserControl.DataContext>
    <e:SystemErrors/>
</UserControl.DataContext>
1 голос
/ 02 декабря 2019

Ну, прежде всего, в вашем SetActiveError методе вы устанавливаете _activeError напрямую вместо ActiveError. RaisePropertyChanged никогда не будет вызван, поэтому ваше представление также не будет обновляться.

0 голосов
/ 03 декабря 2019

В этом была проблема: рассматриваемый пользовательский элемент управления отображается в виде ContentControl на главной странице, но это один из 3 возможных пользовательских элементов управления, которые могут отображаться в том же ContentControl. Я сделал эту работу так, чтобы привязать Содержимое этого CC к свойству под названием CurrentView, управляемым родительской моделью представления. Каждый пользовательский элемент управления имеет пустую модель представления, назначенную ему через шаблон данных в XAML родительской модели представления, поэтому для отображения данного пользовательского элемента управления мы просто назначаем соответствующую пустую виртуальную машину для CurrentView:

Parent Page

<Page.Resources>
    <!-- Set User Control to empty VM -->
    <DataTemplate x:Name="ConnectionErrorViewTemplate" 
       DataType="{x:Type vm:ConnectionErrorViewModel}">
           <v:ConnectionErrorView DataContext="{Binding}" />
    </DataTemplate>
    <DataTemplate x:Name= ...

А затем вниз по странице:

<!-- CC to show user controls -->
<ContentControl x:Name="ConnectionMessagesView" Content="{Binding CurrentView}"/>

Родительская страница VM

// Create new Errorview Instance and show it
ConnectionErrorVM = new ConnectionErrorViewModel();
CurrentView = ConnectionErrorVM;

// Create new Error Instance and populate list
SystemErrors errors = new SystemErrors();
errors.SetActiveError(errorCodeToIndex);

//NOTE:Flipping the order of these has no effect

Итак, как упоминается в последней части ответа mm8, мы называем SetActiveError вParentVM и новый экземпляр SystemErrors создаются пользовательским элементом управления при его отображении. Таким образом, не было ActiveError в том, что касается пользовательского элемента управления, и поэтому нечего было связывать с ним.

Чтобы гарантировать, что мы создаем только один экземпляр класса SystemErrors, который может использоватьсяи parentVM, и пользовательский элемент управления, я только что составил список ошибок, ActiveError и SetActiveError все статические.

SystemErrors

public class SystemErrors : ViewModelBase
{
    public static List<ErrorMessageModel> errors { get; private set; }

    public SystemErrors()
    {  
        errors = new List<ErrorMessageModel>()
        {
            /*** No Error ***/
            new ErrorMessageModel(ErrorCodes.noError, "", "", ""),

            /*** No Devices Found Error ***/
            new ErrorMessageModel(ErrorCodes.noDevicesConnected,
                                  "No Devices Found",
                                  "We couldn't find any attached USB devices.",
                                  "This error occurs ... ")

            /*** Next Error ***/

        };
    }

    private static ErrorMessageModel _activeError;
    public static ErrorMessageModel ActiveError
    {
        get { return _activeError; }
        set
        {
            _activeError = value;
            RaiseActiveErrorChanged(EventArgs.Empty);

        }
    }

    public static event EventHandler ActiveErrorChanged;
    private static void RaiseActiveErrorChanged(EventArgs empty)
    {
        EventHandler handler = ActiveErrorChanged;

        if (handler != null)
            handler(null, empty);
    }

    public static void SetActiveError (byte index)
    {
        ActiveError = errors[index];
    }
}

Единственный сложный бит, который нужно было создатьверсия RaisePropertyChanged (RaiseActiveErrorChanged), чтобы пользовательский элемент управления мог получать событие изменения свойства из статического свойства.

Я не уверен, что это лучший способ, я могу попробовать сделать класс SystemErrorСинглтон или исследуйте более чистый способ показать пользовательские элементы управления.

Любые предложения будут приветствоваться, так как я все еще нахожусь всего лишь несколько недель в C # / WPF / MVVM!

...