Reputation: 197
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.
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).
...
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.
<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.
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?
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
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
:
<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}"/>
// 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.
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
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
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