0

The software uses a SmartSnowUser object, which contains a SecurityRole object. The client needs SecurityRole to be customizable, so it has a list of enum SecurityTasks, which the clients can add/remove from. Controls should only be visible if their given SecurityTask exists in the current SmartSnowUser's SecurityRole.

With this setup, I am struggling to get all the functionality I need. I need the ability to:

  • change control visibility based on whether CurrentUser.Role contains GivenTask
  • make control visibility more granular when necessary (e.g. Visibility &= isInEditMode)
  • meet the previous two requirements without having to create a separate style for each color/task/extra-qualifier combination

Here are the two main approaches I've tried.

Attempt #1:

Wpf User Security Strategy

Current issue: visibility is not being triggered; breakpoint in Convert() method is never hit

Long-term issue: Uses style, so every other custom style will need to be BasedOn this default; also, have to duplicate functionality for editability

Code:

/**Style.xaml**/
<local:TagToVisibilityConverter x:Key="TagToVisibilityConverter"/>
<Style TargetType="{x:Type FrameworkElement}">
    <Setter Property="Visibility">
        <Setter.Value>
            <MultiBinding Converter="{StaticResource TagToVisibilityConverter}">
                <Binding Path="MainData.CurrentUser"/>
                <Binding RelativeSource="{RelativeSource Mode=Self}"/>
            </MultiBinding>
        </Setter.Value>
    </Setter>
</Style>

/**Style.xaml.cs**/
public class TagToVisibilityConverter : IMultiValueConverter
{
    public object Convert(object[] values, Type targetType, object parameter, CultureInfo culture)
    {
        if (values.Length >= 2 && (values[1] as FrameworkElement).GetValue(SecurityLevel.RequiredTaskProperty) is SecurityTask requiredTask)
        {
            //If element has a task assigned and user is not logged in, do not show
            if (values[0] is SmartSnowUser currentUser && currentUser.Role != null)
            {
                return currentUser.Role.Tasks.Contains(requiredTask) ? Visibility.Visible : Visibility.Collapsed;
            }
            return Visibility.Collapsed;
        }

        //If element has no task assigned, default to visible
        return Visibility.Visible;
    }

    public object[] ConvertBack(object value, Type[] targetTypes, object parameter, CultureInfo culture)
    {
        throw new NotImplementedException();
    }
}

public class SecurityLevel
{
    public static readonly DependencyProperty RequiredTaskProperty = DependencyProperty.RegisterAttached("RequiredTask", typeof(SecurityTask), typeof(FrameworkElement), new PropertyMetadata(SecurityTask.ControlBasic));
    public static void SetRequiredTask(UIElement element, SecurityTask value)
    {
        element.SetValue(RequiredTaskProperty, value);
    }

    public static SecurityTask GetRequiredTask(UIElement element)
    {
        return (SecurityTask)element.GetValue(RequiredTaskProperty);
    }
}

/**Implementation in User Control**/
<Button Name="BtnNew" Content="Create New Role" Style="{StaticResource ButtonBlue}" server:SecurityLevel.RequiredTask="{x:Static enums:SecurityTask.EditRoles}" />


Attempt #2:

How to extend instead of overriding WPF Styles

How to add dependency property to FrameworkElement driven classes in WPF?

Attempted to merge these two solutions into one. Set the Tag value to a SecurityTask, then use trigger to set visibility

issue: cannot set visibility at a more granular level without a style (e.g. cannot set 'Visibility' property directly in control); cannot distinguish between visibility/editability

Code:

/**Style.xaml**/
<!--#region Visibility-->
<!-- Default frameworkElement style definition -->
<local:TagToVisibilityConverter x:Key="TagToVisibilityConverter"/>
<Style TargetType="{x:Type FrameworkElement}">
    <Setter Property="Visibility">
        <Setter.Value>
            <MultiBinding Converter="{StaticResource TagToVisibilityConverter}">
                <Binding Path="Tag" RelativeSource="{RelativeSource Mode=Self}"/>
                <Binding Path="MainData.CurrentUser"/>
            </MultiBinding>
        </Setter.Value>
    </Setter>
</Style>

<!-- Extending default style -->
<Style x:Key="ButtonBasic" TargetType="Button" BasedOn="{StaticResource {x:Type FrameworkElement}}">
    <Setter Property="Background" Value="{StaticResource BrushGreyDark}" />
    <Setter Property="Foreground" Value="{StaticResource BrushWhite}" />
</Style>

<Style x:Key="ButtonBlue" TargetType="Button" BasedOn="{StaticResource ButtonBasic}">
    <Setter Property="Background" Value="{StaticResource BrushBlue}" />
</Style>

/**Style.xaml.cs**/
public class TagToVisibilityConverter : IMultiValueConverter
{
    public object Convert(object[] values, Type targetType, object parameter, CultureInfo culture)
    {
        if (values.Length >= 2 && values[0] is SecurityTask requiredTask)
        {
            //If element has a task assigned and user is not logged in, do not show
            if (values[1] is SmartSnowUser currentUser && currentUser.Role != null)
            {
                return currentUser.Role.Tasks.Contains(requiredTask) ? Visibility.Visible : Visibility.Collapsed;
            }

            return Visibility.Collapsed;
        }

        //If element has no task assigned, default to visible
        return Visibility.Visible;
    }

    public object[] ConvertBack(object value, Type[] targetTypes, object parameter, CultureInfo culture)
    {
        throw new NotSupportedException();
    }
}

/**Implementation in User Control**/
//This button works great. Exactly what I need.
<Button Name="BtnNew" Content="Create New Role" Style="{StaticResource ButtonBlue}" Tag="{x:Static enums:SecurityTask.EditRoles}" />

//This button does not work, because the newly set Visibility property overrides the style. 
<Button Name="BtnEdit" Content="Edit Role" Style="{StaticResource ButtonBlue}" Tag="{x:Static enums:SecurityTask.EditRoles}" Visibility="{Binding IsEditMode, Converter={StaticResource InverseBoolToVisibilityConverter}}" />

Attempt #2 ALMOST works. It's that last stinking button, BtnEdit. It is far too cluttered to create a new style - BasedOn ButtonBlue, which is BasedOn ButtonDefault, which is BasedOn our original up there - every time I need to add another qualifier to my visibility setting.

I seem to be over-complicating this. Is there a cleaner approach to what I'm trying to do?

  • Have you considered using an attached property? That is the approach I would take for this. Having billions of styles is too messy. – SledgeHammer Jan 06 '19 at 02:40
  • @SledgeHammer my first attempt included an attached property. The issue was that I have scenarios where I need visibility to be set based on the security level, but also something else (e.g. is in edit mode). – Emily Stammel Jan 07 '19 at 18:54
  • If different controls have different visibility requirements then you'll have to do different things. You can use a multi binding with an attached property. Or multiple attached properties, but then you run into the issue where the user has to know the order to put them in the xaml. – SledgeHammer Jan 07 '19 at 21:12
  • I found a workaround for the issue I brought up before (any button that's only visible in Edit mode, I wrap in a StackPanel). But now with my attached property, it won't update from binding. I think that's because I was trying to avoid having to assign a Style to every framework element. In order to make my attached property update from Binding, will I need to apply a style? – Emily Stammel Jan 08 '19 at 15:14

0 Answers0