canice
canice

Reputation: 373

Binding WPF control visibility using multiple variables from the ViewModel

I'm writing a WPF user control which displays a dynamically generated TabControl with multiple pages, each page in turn contains a list of dynamically generated controls (TextBox, Checkbox etc).

I want to set the visibility of the TextBox, CheckBox controls based on whether the user has permission to view them or not. This permission is a combination of a value on each controls ViewModel ('VisiblyBy') and also a property of the overall UserControl ViewModel ('UserRole'). I'm just getting started on WPF but the standard method seems to be to use a ValueConvertor - however I don't understand how I could write one which would combine/access the different properties (VisiblyBy and UserRole) as they exist at different levels in my ViewModel hierary.

Here is part of the XAML where I bind to the ViewModel:

<TabControl ItemsSource="{Binding VariableData.Pages}" SelectedIndex="0">
<!-- this is the header template-->
   <TabControl.ItemTemplate>
  <DataTemplate>
     <TextBlock Text="{Binding Title}" FontWeight="Bold"/>
  </DataTemplate>
</TabControl.ItemTemplate>
            
<!-- this is the tab content template-->
   <TabControl.ContentTemplate>
  <DataTemplate>
     <StackPanel>
        <ListBox  Grid.IsSharedSizeScope="True" 
                  ItemsSource="{Binding Variables}"
                  ItemTemplateSelector="{StaticResource templateSelector}">
        </ListBox>
        <StackPanel Orientation="Horizontal" HorizontalAlignment="Right">
           <Button Content="Cancel" />
               <Button Content="Submit" 
                       Command="{Binding DataContext.CommitChangesCommand, 
                                   RelativeSource={RelativeSource FindAncestor, 
                                   AncestorType={x:Type TabControl}}}" />
        </StackPanel>
    </StackPanel>
</DataTemplate>
  </TabControl.ContentTemplate>
</TabControl>

I would also need to extend the number of variables that control visibility in the future as it would also depend where in the application it is used from.

Upvotes: 5

Views: 12727

Answers (4)

ΩmegaMan
ΩmegaMan

Reputation: 31721

Bind to a composite property which holds the status instead without the need for a converter.


I would create a composite property on the ViewModel called IsAuthorized and just bind to that property. For it always return the current status when the other properties are set.

How?

To accomplish the composite property the Role and IsOther properties also call PropertyChange on the IsAuthorized property; which always keeps the status fresh on the page.

public Visiblity IsAuthorized
{
   get { return  (Role == "Admin" && OtherProp == "True") 
                     ? Visbility.Visible 
                     : Visibility.Hidden; }

}

 // When a value changes in Role or OtherProp fire PropertyChanged for IsAuthorized. 
public string Role
{
   get { return_Role;}
   set { _Role = value; 
         PropertyChanged("Role");
         PropertyChanged("IsAuthorized");
       }
}

public string OtherProp
{
   get { return_OtherProp;}
   set { _OtherProp = value; 
         PropertyChanged("OtherProp");
         PropertyChanged("IsAuthorized");
       }
}

People seem to think that one has to only bind to a specific property(ies), but why make your life harder when a simply call to PropertyChanged with a composite property will do the job.

Upvotes: 4

ΩmegaMan
ΩmegaMan

Reputation: 31721

What if we transferred the Inversion of Control method to check security/visibility away from the Converter and on to the actual object?

To do that we would need to have the entity instance report its visibility without the need of a converter but still use the VM to provide authorization in a IOC dependency injected method in a two part system.


Let me explain by code:

Using the motif of access levels

public enum SecurityLevels
{
    publicLevel = 0,
    userLevel,
    adminLevel
}

The VM will still contain the current Security level (ultimately based on the actual users login role?) along with a method to report if an accessing instance has the right to access/be shown for the current level. This will be defined in an interface which the VM has to adhere to named IAuthorize.

public interface IAuthorize
{
    SecurityLevels CurrentLevel { get; set; }
    bool GetAuthorization(IAmIAuthorized instance);
}

The IAmIAuthorized (Am-I-Authorized) interface will be required of every entity to be shown and is the second part of the two-part security. Notice the Dependency Injection function DetermineAuthorizationFunc which will eventually be supplied from the VM via Dependency Injection:

public interface IAmIAuthorized
{
   SecurityLevels Level { get; }

   Visibility IsVisible { get; }

   bool IsAuthorized { get; }

   Func<IAmIAuthorized, bool> DetermineAuthorizationFunc { get; set; }
}

Now the entity object if trusted can derive IAmIAuthorzied and report visibility via the DetermineAuthorizationFunc or we can create a wrapper or a derived class with the interface. In any case, every entity to be shown has to have the above interface in one form or another and call DetermineAuthorizationFunc.

Our Xaml becomes easier for we know that the entities' IsVisible is doing its job and returning a Visibility based on the current VM state with its pre-req state.

 <TextBlock Text="{Binding Name}" Visibility="{Binding IsVisible}" />

With that we have moved the IOC away from a Conversion method and into the VM.

Here is an actual implementation of the OP's VariableDataItem class in this situation:

public class VariableDataItem  : IAmIAuthorized
{
    public string Name { get; set; }

    public SecurityLevels Level { get; set; }

    public Func<IAmIAuthorized, bool> DetermineAuthorizationFunc { get; set; }

    public bool IsAuthorized
    {
        get { return DetermineAuthorizationFunc != null && 
                     DetermineAuthorizationFunc(this); }
    }

    public Visibility IsVisible
    {
        get { return IsAuthorized ? Visibility.Visible : Visibility.Hidden; }
    }    
}

With code from the VM

Holdings = new List<VariableDataItem>()
{
    new VariableDataItem() {Name = "Alpha", DetermineAuthorizationFunc = GetAuthorization, Level = SecurityLevels.publicLevel  },
    new VariableDataItem() {Name = "Beta",  DetermineAuthorizationFunc = GetAuthorization, Level = SecurityLevels.userLevel    },
    new VariableDataItem() {Name = "Gamma", DetermineAuthorizationFunc = GetAuthorization, Level = SecurityLevels.adminLevel   }
};

And the IOC dependency injected method:

public bool GetAuthorization(IAmIAuthorized instance)
{
    return (int)instance.Level <= (int)CurrentLevel; 
}

Under that system if the current state is userLevel our list will only show Alpha (which is public) and Beta (which is User) but not Gamma which is admin only:

enter image description here

Admin can see all:

enter image description here

Upvotes: 0

canice
canice

Reputation: 373

Just for completeness, here is the MultiValueConverter I ended up using - note I need add in better error checking on the object array but the idea works.

public class AccessLevelToVisibilityConverter : MarkupExtension, IMultiValueConverter
{
    public object Convert( object[] values, Type targetType, object parameter, CultureInfo culture)
    {
        var visibility = Visibility.Hidden;

        var viewModel = (VariableDataViewModel)values[0];
        var item = (VariableDataItem)values[1];

        visibility = viewModel.IsVariableVisible(item);

        return visibility;
    }

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

    public override object ProvideValue(IServiceProvider serviceProvider)
    {
        return this;
    }
}

And this is the corresponding XAML:

<Grid.Visibility>
   <MultiBinding Converter="{p:AccessLevelToVisibilityConverter}" >
      <Binding Path="DataContext",  
               RelativeSource="{RelativeSource 
                                AncestorType=UserControl}" />
      <Binding Path="." />
   </MultiBinding>
</Grid.Visibility>

I need to apply it to multiple DataTemplates so I guess the way to do that is through a style.

Upvotes: 1

Ayyappan Subramanian
Ayyappan Subramanian

Reputation: 5366

You can try IMultiValueConverter and use Multibinding.

<Window x:Class="ItemsControl_Learning.MainWindow"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    xmlns:local="clr-namespace:ItemsControl_Learning"
    Title="MainWindow" Height="350" Width="525">
<Window.Resources>
    <local:VisibilityConverter x:Key="conv" />
</Window.Resources>
<Grid>
    <Button Content="Test">
        <Button.Visibility>
            <MultiBinding Converter="{StaticResource conv}">
                <Binding  Path="Role" />
                <Binding Path="OtherProp" />                   
            </MultiBinding>
        </Button.Visibility>
    </Button>
</Grid>

public partial class MainWindow : Window
{

    public MainWindow()
    {
        InitializeComponent();
        this.DataContext = new MainViewModel();
    }       
}

class MainViewModel
{
    private string role;
    public string Role
    {
        get { return role; }
        set { role = value; }
    }

    private string otherProp;
    public string OtherProp
    {
        get { return otherProp; }
        set { otherProp = value; }
    }
    public MainViewModel()
    {
        Role = "Admin";
        OtherProp = "Fail";
    }
}

class VisibilityConverter : IMultiValueConverter
{
    public object Convert(object[] values, Type targetType, object parameter, System.Globalization.CultureInfo culture)
    {
        if (values[0].ToString().Equals("Admin") && values[1].ToString().Equals("Pass"))
        {
            return Visibility.Visible;
        }
        else
        {
            return Visibility.Collapsed;
        }
    }

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

In the Convert(...) method, the order of the different inputs in the values array is the same order as in the MultiBinding.Bindings collection.

In this example values[0] contains the Role property and values[1] will be OtherProp because that is the order they got inserted in XAML

Upvotes: 12

Related Questions