liquidair
liquidair

Reputation: 197

WPF MVVM: How to setup binding on user controls?

I'm having a problem grasping how binding to user controls works and why it seems to work differently than on pages. What I'm trying to do is create an error display (Error Name, Description and Tips, which tells how to troubleshoot it) that will show from a content control if there's an error, or other stuff if there's no error.

I'm doing this with a user control that will essentially be a sub-view on a page to avoid having rude popups, and will be reused on multiple pages. I have the content control binding working so we get the user control displayed, just no info.

For 'DRY' purposes, I've created an Error Model with the desired properties and then use a class to implement this model as a list of errors. In the constructor, I simply add new errors to the list...this way all of the app's errors are in the same place for easy maintenance.

System Error class:

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];

}

In the page's view model, we use an enum ErrorCodes to have a name pointing to the index of the error. So when we have an error, we pass the errorCode to a method that casts it as a byte and then calls 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);
}

Now the idea here is to set the user control's data context to SystemError and thus bind to ActiveError (ActiveError.ErrorName, ActiveError.ErrorDescription, etc.). My thinking was this would allow us to use a single datacontext because no matter what page we are on when we have an error, the error info always comes from SystemErrors.

User Control:

<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>

But, I can't seem to get it to work. You can see all of my leftover failed approaches in the XAML, but this only scratches the surface of what I've tried. I can get this to work if I cut the guts of the UC out and paste it in the page, so what that tells me is binding to a page has a different mechanism than to a user control.

I've read a bunch of tutorials, watched a few videos, but all of them kind of skip over the how it works bit; its always "to make this work, we need this already working code" which only helps if you have the exact same problem. I've seen dependency properties, what appears to be normal binding, relative source to self, relative source to ancestor, etc.

Questions:

So why does a user control appear to have a different binding mechanism than windows/pages (why does the data context not work like it does elsewhere)? If we need dependency properties, then why don't we need them for binding to pages? And also in regards to DPs if needed, in this case, would I just make an ActiveErrorProperty of type ErrorModel, or do we need one for each sub-property (ErrorName of type string)? How do we link the DP to the property we want to bind to?

Update:

Tried all day today to get this working so I began tracing and outputting to the console. There were both no binding errors, and if I stuck Trace.WriteLine in the public declaration of ActiveError after RaisePC(), ActiveError would be set to the correct error. Then I tried tracing the binding in the XAML and there's some interesting things:

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 ''

Note that it shows that ActiveError is set correctly (first two lines, "No Devices Found" is the ErrorName) before we see the binding fail. I'm too new to WPF but if I'm interpreting the trace correctly it looks like it finds ActiveError in the datacontext SystemErrors but fails to get anything from ActiveError.ErrorName, which we know is set to the correct value. What's that about?

Upvotes: 1

Views: 357

Answers (3)

liquidair
liquidair

Reputation: 197

Here was the problem: The user control in question is shown in a ContentControl on the main page, but it is one of 3 possible user controls that can be shown in that same ContentControl. The way I made this work was to bind the Content of this CC to a property called CurrentView managed by the parent viewmodel. Each user control has an empty viewmodel assigned to it via a data template in the parent viewmodel's XAML, so to show a given user control we just assign the corresponding empty VM to 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= ...

And then later down the page:

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

Parent Page 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

So as the last part of mm8's answer mentions, we call SetActiveError in the ParentVM, and a new instance of SystemErrors is being created by the user control when it is shown. So there was no ActiveError as far as the user control was concerned, and thus there was nothing to bind to.

In order to ensure we only create a single instance of the SystemErrors class that can be used by both the parentVM and the user control, I just made the list of errors, ActiveError and SetActiveError all static.

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];
    }
}

The only tricky bit was having to create a version of RaisePropertyChanged (RaiseActiveErrorChanged) so the user control can get a property change event from a static property.

I'm not sure if this is the best way, I may try making the SystemError class a singleton or investigate a cleaner way to show the user controls.

Any suggestions would be welcome as I'm still only a few weeks in to C#/WPF/MVVM!

Upvotes: 0

mm8
mm8

Reputation: 169420

SystemErrors is not a visual ancestor of the UserControl. It is the DataContext so the following should work as far as the binding is concerned provided that the ErrorMessageModel class has a public ErrorName property that returns what you expect it to return:

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

The following will however not set the ErrorMessageModel property and raise the PropertyChanged event:

_activeError = errors[index];

You should set the property to a new ErrorMessageModel object:

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

Also make sure that you call the SetActiveError method on the actual instance of the SystemErrors class that you create in your XAML markup:

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

Upvotes: 2

Mark Feldman
Mark Feldman

Reputation: 16148

Well first of all in your SetActiveError method you're setting _activeError directly instead of ActiveError. RaisePropertyChanged will never get invoked, so your view won't update either.

Upvotes: 1

Related Questions