Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

WPF bind IsEnabled to method on view model

I am working on a WPF application that is using claims for controlling what users are and are not allowed to do. The requirement is that controls that users don't have access to are disabled. In our base view model we have the following method:

HasClaim(string name);

I'd like to do something like this in the views:

<button IsEnabled="{Binding HasClaim("NAME")}" />

I know I could an ObjectDataProvider but I don't want to create one for every claim.

like image 735
Nick Avatar asked Apr 11 '16 19:04

Nick


2 Answers

If you do this often in your views, consider using markup extension. Most likely you do not need any information from view or view model to check if user has the right claim, usually such information is fetched on log in and is not dependant on concrete view.

public class HasClaimExtension : MarkupExtension {
    private readonly string _name;
    public HasClaimExtension(string name) {
        _name = name;
    }

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

    private bool HasClaim() {
        // check if user has this claim here
        if (_name.ToLowerInvariant() == "admin")
            return true;
        return false;
    }
}

Then just:

<Button IsEnabled="{local:HasClaim Admin}" Height="20" Width="100"/>

In the unlikely case you really need access to your view model, you can do that still:

public class HasClaimExtension : MarkupExtension {
    private readonly string _name;

    public HasClaimExtension(string name) {
        _name = name;
    }

    public override object ProvideValue(IServiceProvider serviceProvider) {
        var service = (IProvideValueTarget) serviceProvider.GetService(typeof (IProvideValueTarget));
        // this is Button or whatever control you set IsEnabled of
        var target = service.TargetObject as FrameworkElement;
        if (target != null) {
            // grab it's DataContext, that is your view model
            var vm = target.DataContext as MyViewModel;
            if (vm != null) {
                return vm.HasClaim(_name);
            }
        }
        return false;
    }
}

public class MyViewModel {
    public bool HasClaim(string claim) {
        return false;
    }
}

UPDATE to answer your question in comment. You can do it like this. Suppose some simple LoginManager class:

public class LoginManager {
    public static LoginManager Instance = new LoginManager();

    private LoginManager() {

    }

    public bool IsLoggedIn { get; private set; }

    public void Login() {
        // do something, then
        IsLoggedIn = true;
        OnLoggedIn?.Invoke();
    }

    public bool HasClaim(string name) {
        if (!IsLoggedIn)
            throw new Exception("Cannot check claim until logged in");
        return true;
    }

    public event Action OnLoggedIn;
}

It has some indication about if claims are already available, and also an event to notify when those claims are available if now they are not. Then in your markup extension you first check if claims are here. If yes - just return result already. If not - return false, but subsribe to the event when those claims become available. After that - update target property with real value.

public class HasClaimExtension : MarkupExtension {
    private readonly string _name;

    public HasClaimExtension(string name) {
        _name = name;
    }

    public override object ProvideValue(IServiceProvider serviceProvider) {
        if (LoginManager.Instance.IsLoggedIn) {
            return LoginManager.Instance.HasClaim(_name);
        }
        // if not logged in yet
        var service = (IProvideValueTarget) serviceProvider.GetService(typeof (IProvideValueTarget));
        var target = service.TargetObject as FrameworkElement;
        // this is dependency property you want to set, IsEnabled in this case
        var targetProperty = service.TargetProperty as DependencyProperty;
        if (target != null && targetProperty != null) {
            if (targetProperty.PropertyType != typeof (bool)) {
                // not boolean property - throw
                throw new Exception("HasClaim extension should be applied to Boolean properties only");
            }
            // here, subscribe to event after which your claims are available
            LoginManager.Instance.OnLoggedIn += () => {
                // update target property
                if (Application.Current.Dispatcher.CheckAccess())
                    target.SetValue(targetProperty, LoginManager.Instance.HasClaim(_name));
                else {
                    Application.Current.Dispatcher.Invoke(() => {
                        target.SetValue(targetProperty, LoginManager.Instance.HasClaim(_name));
                    });
                }
            };
        }

        return false;
    }
}
like image 52
Evk Avatar answered Nov 05 '22 03:11

Evk


You can use a MultiValueConverter and pass the Button itself and its DataContext as bindings:

<Button Name="someName">
    <Button.IsEnabled>
        <MultiBinding Converter={StaticResource HasClaimConverter}>
            <Binding Path="Name" RelativeSource="{RelativeSource Self}"/>
            <Binding/>
        </MultiBinding>
    </Button.IsEnabled>
</Button>

And the converter class:

class HasClaimConverter : IMultiValueConverter
{
    public object Convert(object[] values, Type targetType, object parameter, CultureInfo culture)
    {
        var name= values[0] as String;
        var vm = values[1] as YourViewModel;

        return YourViewModel.HasClaim(name);
    }

    public object[] ConvertBack(object value, Type[] targetTypes, object parameter, CultureInfo culture)
    {
        throw new NotImplementedException();
    }
}
like image 42
heltonbiker Avatar answered Nov 05 '22 03:11

heltonbiker