简体   繁体   中英

XAML Binding in Style Setter using the Binding Path from the target control

I had an interesting request from a client, there were a number challenges involved, the one that I thought would be the easiest, turned out to be the hardest.

The user needs to know that a value has been changed locally, but not yet persisted to the backend store. (A dirty state) We solved this with a data trigger on a style declared within each control on the page. The control background will be filled with yellow when the value is changed, then reset back to the control default when the save button is pressed.

The ModelView implements a custom interface : ILocalValueCache This has an indexer that should return Boolean to indicate if the current value has changed since the last data refresh.

  • The ModelView also Implements IDataErrorInfo and uses DataAnnotations Attributes for validation, so I can't simply use validation templates.

What I would like to do is simplify the XAML using a single Style or a control template this is hard because each control now has two bindings, one to Value and another to Value IsLocal :

To be more specific, I would prefer a solution where the developer doesn't need to know the inner mechanics of how it works (what the field names for the xIsLocal flags are) or if the binding source even supports it.

Because the ViewModel implements this interface, (like IDataErrorInfo) I should be able to globally target control styles bound to the states described by the interface.

Here is a section of the XAML with some of the textboxes:

         <TextBox Text="{Binding ScaleName}" Margin="5,2,5,2" Grid.Row="1" Grid.Column="2">
            <TextBox.Style>
                <Style TargetType="{x:Type TextBox}">
                    <Style.Triggers>
                        <DataTrigger Binding="{Binding ScaleNameIsLocal}" Value="True">
                            <Setter Property="Background" Value="{StaticResource LocalValueBackgroundBrush}" />
                        </DataTrigger>
                    </Style.Triggers>
                </Style>
            </TextBox.Style>
        </TextBox>
        <TextBox x:Name="nbScaleCap" Text="{Binding ScaleCap}" Grid.Row="3" Grid.Column="0" Margin="5,2,5,2">
            <TextBox.Style>
                <Style TargetType="{x:Type TextBox}">
                    <Style.Triggers>
                        <DataTrigger Binding="{Binding ScaleCapIsLocal}" Value="True">
                            <Setter Property="Background" Value="{StaticResource LocalValueBackgroundBrush}" />
                        </DataTrigger>
                    </Style.Triggers>
                </Style>
            </TextBox.Style>
        </TextBox>
        <TextBox x:Name="nbTareTol" Text="{Binding TareTol}" Grid.Row="3" Grid.Column="1" Margin="5,2,5,2">
            <TextBox.Style>
                <Style TargetType="{x:Type TextBox}">
                    <Style.Triggers>
                        <DataTrigger Binding="{Binding TareTolIsLocal}" Value="True">
                            <Setter Property="Background" Value="{StaticResource LocalValueBackgroundBrush}" />
                        </DataTrigger>
                    </Style.Triggers>
                </Style>
            </TextBox.Style>
        </TextBox>

As well as the indexer, each property on the View Model has an xxxIsLocal reciprocal property, So the following partial model corresponds to the above example:

string ScaleName { get; set; }
bool ScaleNameIsLocal { get; set; }
string ScaleCap { get; set; }
bool ScaleCapIsLocal { get; set; }
string TareTol { get; set; }
bool TarTolIsLocal { get; set; }

I've played around with using the indexer on the interface to get the IsLocal value but struggled with INotifyPropertyChanged implementation (getting the model to raise the indexer value changed event), that aside the bigger issue was how to make a single style with a binding that is based on the path of the content or text binding on the target control instead of the value of the binding result.

I was inspired by the IDataErrorInfo pattern and using the Validation.ErrorTemplate, it looks simple on the surface and such a simple repetitive pattern like this seems like something that WPF should be able to handle without too many issues.

I am not sure how often I will need this exact template, but it's a pattern that I'm sure I'd like to use again, where there is a potential for each property to have multiple states (not just Error) and to apply a different style using the state as a trigger.


I've edited this post because I haven't quite found what I wanted but thanks to Nikkita I am a step closer :)

By using a custom attached property, we can declare the binding to the flag field directly in the control and can now properly define the style triggers in a global style dictionary.

The ViewModel has not changed, but the XML from above is now simplifed:

    <Style TargetType="TextBox" BasedOn="{StaticResource {x:Type TextBox}}">
        <Style.Triggers>
            <Trigger Property="att:IsLocal.Value" Value="True">
                <Setter Property="Background" Value="{StaticResource LocalValueBackgroundBrush}" />
            </Trigger>
        </Style.Triggers>
    </Style>

<TextBox Text="{Binding ScaleName}" Margin="5,2,5,2" Grid.Row="1" Grid.Column="2" att:IsLocal.Value="{Binding ScaleNameIsLocal}"></TextBox>
<TextBox Text="{Binding ScaleCap}" Grid.Row="3" Grid.Column="0" Margin="5,2,5,2" att:IsLocal.Value="{Binding ScaleCapIsLocal}"></TextBox>
<TextBox Text="{Binding TareTol}" Grid.Row="3" Grid.Column="1" Margin="5,2,5,2" att:IsLocal.Value="{Binding TareTolIsLocal}"></TextBox>

My biggest issue with the current solution is that I would still need to edit a lot of existing XAML if I wanted to apply this (or another) interface pattern to existing apps. Even in the current form there are over 20 fields, so that's 20 opportunities to get the binding wrong, or to accidentally skip one.

I would suggest you the "validator" pattern (look to spec INotifyDataErrorInfo) combined with custom Behaviour. Validator crates the collection with results according bound property names in item and Bahaviour change the element. Check the MSDN help.

Xaml Example:
                    <TextBox 
                             Name="a"
                    Text="{Binding *Variable*, Mode=OneWay}"
                    Header="Start"
                    Style ="{StaticResource MitfahrenDefaultTextEdit}" IsReadOnly="true" 
                    Tapped="StartLocation_OnTapped">
                        <interactivity:Interaction.Behaviors>
                            <behaviors:RedFormFieldOnErrors 
                                PropertyErrors="{Binding Path=Record.ValidationCollection[*Variable*]}"/>
                        </interactivity:Interaction.Behaviors>
                    </TextBox>

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM