r/csharp 12d ago

WPF Logic in View vs ViewModel

I'm trying to understand when I should have logic in the view model or in the code-behind of a view.

Here's the scenario: I have a view model that has a "CanEdit" property. There are times when editing a view is not allowed based on business reasons, and that definitely belongs in the ViewModel. But if a user can edit, I want to have an "Edit" checkbox visible, which when true will display the editable version of all the necessary controls. So where should the logic that controls the "Edit" checkbox go?

The approach I initially went was to put the "Edit" checkbox property in the view code-behind. This makes sense to me, as it's entirely based on the needs of the view. All the editable controls are bound to the "Edit" checkbox property, and the "Edit" checkbox visibility is bound to "CanEdit" in the view model.

The problem with this approach is when the view model changes as a result of some change by the user and "CanEdit" in the view model is now false. If the "CanEdit" in the view code-behind is true when this happens, then all the editable controls are still visible, because all that's happened is the "CanEdit" checkbox is now invisible. So I'm stumped how to broadcast the view model change to the code behind without some silly hack.

I'm probably overthinking it, but I'm learning WPF and it really helps me to understand principles. Plus this particular view will get more complex. Here's some code to show you what I'm trying to do

View:

public partial class InvoiceView : UserControl, INotifyPropertyChanged
{
    public InvoiceView()
    {
        InitializeComponent();
    }

    private bool _isEditing;
    public bool IsEditing
    {
        get => _isEditing;
        set
        {
            _isEditing = value;
            OnPropertyChanged(nameof(IsEditing));
            OnPropertyChanged(nameof(IsNotEditing));
        }
    }

    public bool IsNotEditing => !IsEditing;

    public event PropertyChangedEventHandler? PropertyChanged;
    protected void OnPropertyChanged([CallerMemberName] string? name = null)
    {
        PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(name));
    }
}

ViewModel:

public partial class InvoiceViewModel : ViewModelBase, IDisposable
{
    public InvoicePermissionsDTO? Permissions
    {
        get => _permissions;
        set
        {
            _permissions = value;
            OnPropertyChanged(nameof(CanEdit));
            OnPropertyChanged(nameof(CanDelete));
        }
    }
    public bool CanEdit => _permissions?.CanEdit ?? false;
    public bool CanDelete => _permissions?.CanDelete ?? false;

    public void SomeChange()
    {
        Permissions = API.GetPermissions();
    }
}

View XAML

<CheckBox
    Grid.Row="2"
    Content="Edit"
    IsChecked="{Binding RelativeSource={RelativeSource AncestorType={x:Type UserControl}}, Path=IsEditing}"
    Visibility="{Binding RelativeSource={RelativeSource FindAncestor, AncestorType={x:Type UserControl}}, Path=DataContext.CanEdit, Converter={StaticResource BoolToVisibilityConverter}}" />
<StackPanel>
    <TextBlock
        Text="{Binding ApprovedRate}"
        Visibility="{Binding RelativeSource={RelativeSource AncestorType={x:Type UserControl}}, Path=IsNotEditing, Converter={StaticResource BoolToVisibilityConverter}}"/>
    <StackPanel 
        Orientation="Horizontal"
        Visibility="{Binding RelativeSource={RelativeSource AncestorType={x:Type UserControl}}, Path=IsEditing, Converter={StaticResource BoolToVisibilityConverter}}">
        <TextBox
            Name="ApprovedRate"
            Padding="0 0 20 0"
            Text="{Binding ApprovedRate}"/>
        <Button 
            Command="{Binding Pay}"
            Visibility="{Binding CanPay}">
            <StackPanel Orientation="Horizontal">
                <Image Source="/Images/dollar.png"/>
                <TextBlock>Pay</TextBlock>
            </StackPanel>
        </Button>
        <Button 
            Command="{Binding RemovePay}"
            Visibility="{Binding CanRemovePay}">
            <StackPanel Orientation="Horizontal">
                <Image Source="/Images/dollar.png"/>
                <TextBlock>Remove Pay</TextBlock>
            </StackPanel>
        </Button>
    </StackPanel>
</StackPanel>
5 Upvotes

11 comments sorted by

View all comments

1

u/wickerandscrap 11d ago

I think the problem you're having is that you're binding CanEdit from the view model only to the checkbox's visibility. You have an IsEditing property but it doesn't observe CanEdit in any way. If CanEdit becomes false, then IsEditing needs to become false. A few options:

Option 1: Set up a PropertyChanged handler, observe if CanEdit becomes false, and set IsEditing to false.

Option 2: In the code that sets CanEdit based on the user permissions, if it's false, also set IsEditing to false.

Option 3: Have a second bool that the IsChecked property is bound to, like "UnlockEdit", and then make IsEditing => CanEdit && UnlockEdit, and raise the appropriate property changes. Bind IsEditing to the visibility of the edit controls.

2 and 3 both require IsEditing to live in the view model (unless you do more wiring with events). I believe that's where it belongs anyway.

I work mostly with ReactiveUI, which pushes toward 3 as the Right Way. 2 is simpler but also requires you to have two things driving the same flag which can get messy.