C
C#2y ago
Indeed

❔ WPF multiple data contexts

Hi! In my wpf app i need to have view-specific commands but i am already using my separate ViewModel (MVVM) as a data context? How can I achieve it wanting a button do something view-specific but also something on the model?
<Button>
<i:Interaction.Triggers>
<i:EventTrigger EventName="Click">
<i:InvokeCommandAction Command="{Binding ViewModelCommand}" />
<i:InvokeCommandAction Command="{Binding ViewCommand}" /> //
</i:EventTrigger>
</i:Interaction.Triggers>
</Button>
<Button>
<i:Interaction.Triggers>
<i:EventTrigger EventName="Click">
<i:InvokeCommandAction Command="{Binding ViewModelCommand}" />
<i:InvokeCommandAction Command="{Binding ViewCommand}" /> //
</i:EventTrigger>
</i:Interaction.Triggers>
</Button>
21 Replies
Indeed
IndeedOP2y ago
this works for the Click event handler
<Button Click="SayHello" Background="{StaticResource BrushBgHover}">
<TextBlock Text="{Binding Message}" />
</Button>
<Button Click="SayHello" Background="{StaticResource BrushBgHover}">
<TextBlock Text="{Binding Message}" />
</Button>
But binding to message does not work
public partial class WeatherHomeView : UserControl {
public string Message { get; set; } = "Hi";
public WeatherHomeView() {
InitializeComponent();
}

public void SayHello(object sender, RoutedEventArgs e) {
MessageBox.Show("Hello from the view!");
}
}
public partial class WeatherHomeView : UserControl {
public string Message { get; set; } = "Hi";
public WeatherHomeView() {
InitializeComponent();
}

public void SayHello(object sender, RoutedEventArgs e) {
MessageBox.Show("Hello from the view!");
}
}
I've tried doing it like this
<Button Click="SayHello" Background="{StaticResource BrushBgHover}">
<Button.DataContext>
<local:WeatherHomeView/>
</Button.DataContext>
<TextBlock Text="{Binding Message}" />
</Button>
<Button Click="SayHello" Background="{StaticResource BrushBgHover}">
<Button.DataContext>
<local:WeatherHomeView/>
</Button.DataContext>
<TextBlock Text="{Binding Message}" />
</Button>
only to get stackoverflow exception looking into something like this
<UserControl.DataContext>
<local:WeatherHomeView x:Name="SelfContext"/>
</UserControl.DataContext>
<UserControl.DataContext>
<local:WeatherHomeView x:Name="SelfContext"/>
</UserControl.DataContext>
mikernet
mikernet2y ago
Not sure I understanding the question The Message prop should be on the VM And Click should fire a handler just on the VM Why do you need behavior in the view The point of a VM is that it encapsulates the behavior And the view just binds whatever it needs on the VM If you need to be showing message boxes, the right way to do that is to do it from the VM by injecting an IAlert implementation and then calling IAlert.ShowMessage() or whatever
Indeed
IndeedOP2y ago
For my specific situation I have a listview and on scroll all the way to left/right i want to fetch more data my current solution
public partial class WeatherHomeView : UserControl, INotifyPropertyChanged {
public event PropertyChangedEventHandler? PropertyChanged;
private void OnPropertyChanged(string name) {
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(name));
}

private double _maxScrollOffset;
public double MaxScrollOffset {
get => _maxScrollOffset;
set {
if (value == _maxScrollOffset) {
return;
}

_maxScrollOffset = value;
OnPropertyChanged(nameof(MaxScrollOffset));
}
}

private double _scrollOffset;
public double ScrollOffset {
get => _scrollOffset;
set {
if (_scrollOffset == value) {
return;
}

_scrollOffset = value;
OnPropertyChanged(nameof(ScrollOffset));
}
}

public WeatherHomeView() {
InitializeComponent();
}

private void ScrollChanged(object sender, ScrollChangedEventArgs e) {
Debug.WriteLine($"ScrollChanged Offset: {e.HorizontalOffset} VpWidth: {e.ViewportWidth} ExWidth: {e.ExtentWidth}");
ScrollOffset = e.HorizontalOffset;
MaxScrollOffset = e.ExtentWidth - e.ViewportWidth;
}
}
public partial class WeatherHomeView : UserControl, INotifyPropertyChanged {
public event PropertyChangedEventHandler? PropertyChanged;
private void OnPropertyChanged(string name) {
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(name));
}

private double _maxScrollOffset;
public double MaxScrollOffset {
get => _maxScrollOffset;
set {
if (value == _maxScrollOffset) {
return;
}

_maxScrollOffset = value;
OnPropertyChanged(nameof(MaxScrollOffset));
}
}

private double _scrollOffset;
public double ScrollOffset {
get => _scrollOffset;
set {
if (_scrollOffset == value) {
return;
}

_scrollOffset = value;
OnPropertyChanged(nameof(ScrollOffset));
}
}

public WeatherHomeView() {
InitializeComponent();
}

private void ScrollChanged(object sender, ScrollChangedEventArgs e) {
Debug.WriteLine($"ScrollChanged Offset: {e.HorizontalOffset} VpWidth: {e.ViewportWidth} ExWidth: {e.ExtentWidth}");
ScrollOffset = e.HorizontalOffset;
MaxScrollOffset = e.ExtentWidth - e.ViewportWidth;
}
}
<ListView
x:Name="ForecastsListView"
Grid.RowSpan="3"
Grid.Column="1"
HorizontalContentAlignment="Stretch"
VerticalContentAlignment="Stretch"
Background="Transparent"
BorderThickness="0"
ItemsSource="{Binding Weather}"
ScrollViewer.ScrollChanged="ScrollChanged"
SelectedItem="{Binding SelectedWeather}">

<ListView.ItemsPanel>
...
</ListView.ItemsPanel>
<ListView.ItemTemplate>
...
</ListView.ItemTemplate>

<i:DataTrigger
Binding="{Binding ScrollOffset, RelativeSource={RelativeSource AncestorType=local:WeatherHomeView}}"
Value="{Binding MaxScrollOffset, RelativeSource={RelativeSource AncestorType=local:WeatherHomeView}}">
<i:InvokeCommandAction Command="" />
</i:DataTrigger>
<i:DataTrigger
Binding="{Binding ScrollOffset, RelativeSource={RelativeSource AncestorType=local:WeatherHomeView}}"
Value="0">
<i:InvokeCommandAction Command="" />
</i:DataTrigger>
</ListView>
<ListView
x:Name="ForecastsListView"
Grid.RowSpan="3"
Grid.Column="1"
HorizontalContentAlignment="Stretch"
VerticalContentAlignment="Stretch"
Background="Transparent"
BorderThickness="0"
ItemsSource="{Binding Weather}"
ScrollViewer.ScrollChanged="ScrollChanged"
SelectedItem="{Binding SelectedWeather}">

<ListView.ItemsPanel>
...
</ListView.ItemsPanel>
<ListView.ItemTemplate>
...
</ListView.ItemTemplate>

<i:DataTrigger
Binding="{Binding ScrollOffset, RelativeSource={RelativeSource AncestorType=local:WeatherHomeView}}"
Value="{Binding MaxScrollOffset, RelativeSource={RelativeSource AncestorType=local:WeatherHomeView}}">
<i:InvokeCommandAction Command="" />
</i:DataTrigger>
<i:DataTrigger
Binding="{Binding ScrollOffset, RelativeSource={RelativeSource AncestorType=local:WeatherHomeView}}"
Value="0">
<i:InvokeCommandAction Command="" />
</i:DataTrigger>
</ListView>
since I need to use the event to get the value from view specific event and to save it i have to bind to my viewClass for these instances as seen i was able to use ancestorType for it although it is a bit cumbersome to write out Binding="{Binding ScrollOffset, RelativeSource={RelativeSource AncestorType=local:WeatherHomeView}}" other context is assumed to be VM through inheritance I'll gladly take criticism of how it could be done otherwise with respect to mvvm (Gonna think about moving it to VM tmrrw since every collection display afaik has scrollviewer so might be useful to put it there)
mikernet
mikernet2y ago
I would still do this in the VM I made this for exactly this kind of scenario: https://github.com/Singulink/Singulink.WPF.Data.MethodBinding Towards the bottom you can see this example:
You can even bind to properties on the event args themselves and pass them as parameters:
You can even bind to properties on the event args themselves and pass them as parameters:
public class ViewModel
{
public void UpdateSize(Size newSize) { }
}
public class ViewModel
{
public void UpdateSize(Size newSize) { }
}
<!-- Passes in SizeChangedEventArgs.NewSize as the parameter -->
<Canvas SizeChanged="{s:MethodBinding UpdateSize, {s:EventArgs NewSize}}" />
<!-- Passes in SizeChangedEventArgs.NewSize as the parameter -->
<Canvas SizeChanged="{s:MethodBinding UpdateSize, {s:EventArgs NewSize}}" />
So your VM still stays view agnostic and doesn't need to reference any UI assemblies with the event args types in them Your VM method would probably be something like
public void UpdateScroll(double horizontalOffset, double extentWidth, double viewportWidth)
{
}
public void UpdateScroll(double horizontalOffset, double extentWidth, double viewportWidth)
{
}
And then your binding would be:
<ListView ScrollViewer.ScrollChanged="{s:MethodBinding UpdateScroll, {s:EventArgs HorizontalOffset}, {s:EventArgs ExtentWidth}, {s:EventArgs ViewportWidth}" />
<ListView ScrollViewer.ScrollChanged="{s:MethodBinding UpdateScroll, {s:EventArgs HorizontalOffset}, {s:EventArgs ExtentWidth}, {s:EventArgs ViewportWidth}" />
You can also call VM methods from the view codebehind, FYI.
Indeed
IndeedOP2y ago
Oh, that looks great. Will definitely be useful <3. However I think I would like to settle in the end with VM having a method for fetching data and view specifying when it should be called (maybe with a button click, maybe on scroll to the end, generally something view specific)
and on scroll all the way to left/right i want to fetch more data
However you got me interested into calling VM methods from VC (viewclass), how is it achievable? My context from MainWindow is passed this way
mc:Ignorable="d">
<Grid d:DataContext="{d:DesignInstance vms:MainViewModel}">
<Grid.Resources>
<DataTemplate DataType="{x:Type vms:WeatherHomeViewModel}">
<views:WeatherHomeView />
</DataTemplate>
<DataTemplate DataType="{x:Type vms:WelcomeViewModel}">
<views:WelcomeView />
</DataTemplate>
</Grid.Resources>

<ContentControl Content="{Binding CurrentViewModel}" />
</Grid>
mc:Ignorable="d">
<Grid d:DataContext="{d:DesignInstance vms:MainViewModel}">
<Grid.Resources>
<DataTemplate DataType="{x:Type vms:WeatherHomeViewModel}">
<views:WeatherHomeView />
</DataTemplate>
<DataTemplate DataType="{x:Type vms:WelcomeViewModel}">
<views:WelcomeView />
</DataTemplate>
</Grid.Resources>

<ContentControl Content="{Binding CurrentViewModel}" />
</Grid>
do you mean this way?
var x = this.DataContext as WeatherHomeViewModel;
x.method();
var x = this.DataContext as WeatherHomeViewModel;
x.method();
mikernet
mikernet2y ago
I would write a typed prop for the VM in this case that assigns the DC but that's the idea yeah
Indeed
IndeedOP2y ago
oh okay so it would be something here? <ContentControl SomePropertyToStoreViewModel="{Binding CurrentViewModel}" />
mikernet
mikernet2y ago
As for putting behavior in views - I think this boils down to a difference in controls vs views I don't put code behind into views If there's any reason for codebehind then I make a control that I can put into a view Like a real reusable templated control, not a user control I.e. "InfiniteScollListView"
Indeed
IndeedOP2y ago
Oh that's an interesting view, that makes a lot of sense. I do that however it is only at the point of cleaning up my code, when prototyping all goes where it can Hmm, not an user control? templated control haven't heard about it, will definitely have to read on it. Looking at templates in vs only thing sounding similiarily is custom control
mikernet
mikernet2y ago
Like, I would subclass ListView, not UserControl, for example
Indeed
IndeedOP2y ago
oh, i haven't seen it be recommended anywhere 👀 , even wpf unleashed didn't mention a word. Seen most people recommend attached properties
mikernet
mikernet2y ago
That can work in some cases as well Sometimes a new control is cleaner/better though I mean like
public WeatherHomeViewModel ViewModel
{
get => DataContext as WeatherHomeViewModel;
set => DataContext = value;
}
public WeatherHomeViewModel ViewModel
{
get => DataContext as WeatherHomeViewModel;
set => DataContext = value;
}
On the view
Indeed
IndeedOP2y ago
oh simply extracting the cast, okay
mikernet
mikernet2y ago
Yeah I think it's generally better to register DPs on controls instead of using INPC as well INPC should really only be for VMs
Indeed
IndeedOP2y ago
do DPs two-way bound auto implement INPC?
mikernet
mikernet2y ago
If you mean can DPs be two-way bound then yeah, that's how the existing DPs on your controls work They don't implement INPC, it's a different mechanism Mixing the two into one type is a bit of a code smell If its a dependency object with DPs on it then no real reason to use INPC as well DPs are more powerful INPC is a simplified version of DPs for VMs
Indeed
IndeedOP2y ago
Ye, I've had to create my own DP snippet due to using them a lot coz VS still using "propertyName" instead of nameof and 0 instead of default(type) XD
mikernet
mikernet2y ago
Yeah
Indeed
IndeedOP2y ago
Thank you a lot for your help! I've definitely learnt a lot from this conversation. Can't wait to try all of the things you've suggested. Some of the things you mentioned I've not seen in the books, will definitely have to write some of them down 💜<a:slickster_swim:1018447771830857728>
mikernet
mikernet2y ago
No probskis 🙂
Accord
Accord2y ago
Was this issue resolved? If so, run /close - otherwise I will mark this as stale and this post will be archived until there is new activity.
Want results from more Discord servers?
Add your server