ReadOnlyObservableCollection<T> in WPF Design time data - c#

I have a ViewModel that has a property which is a ReadOnlyObservableCollection. Defined something like this:
public class MyViewModel
{
private ObservableCollection<string> myProtectedCollection;
public ReadOnlyObservableCollection<string> MyCollectionProperty { get; }
public MyViewModel()
{
this.myProtectedCollection = new ObservableCollection<string>();
this.MyCollectionProperty = new ReadOnlyObservableCollection<string>(this.myProtectedCollection);
this.myProtectedCollection.Add("String1");
this.myProtectedCollection.Add("String2");
this.myProtectedCollection.Add("String3");
}
}
I have then created a xaml file called TestData.xaml and set the build action to DesignData. In that I have this:
<local:MyViewModel
xmlns:local="clr-namespace:ScrapWpfApplication1"
xmlns:system="clr-namespace:System;assembly=mscorlib">
<local:MyViewModel.MyCollectionProperty>
<system:String>String 1</system:String>
<system:String>String 2</system:String>
</local:MyViewModel.MyCollectionProperty>
</local:MyViewModel>
Finally I have a MainWindow.xaml with the following:
<Window x:Class="ScrapWpfApplication1.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:local="clr-namespace:ScrapWpfApplication1"
mc:Ignorable="d"
Title="MainWindow" Height="350" Width="525"
d:DataContext="{d:DesignData Source=SampleData.xaml}">
<ListBox ItemsSource="{Binding MyCollectionProperty}">
<ListBox.ItemTemplate>
<DataTemplate>
<TextBlock Text="{Binding}" />
</DataTemplate>
</ListBox.ItemTemplate>
</ListBox>
</Window>
The problem is that this is not showing my sample data in the Visual Studio designer. If I change the collection in my view model to be a ObservableCollection instead of a ReadOnlyObservableCollection then it works as expcted.
I guess that this is because the design time data system is creating a dummy ReadOnlyCollection but XAML is unable to populate it because it is readonly.
Is there any way to get the design type data system to work without making my view model's collection property writeable?

Is there any way to get the design type data system to work without making my view model's collection property writeable?
Yes, you could create another view model class, to be used for design purposes only, with an ObservableCollection<T> property, and set the design time DataContext of the view to an instance of this one:
d:DataContext="{d:DesignInstance Type=local:DesignTimeViewModel, IsDesignTimeCreatable=True}

I've not seen a perfect answer to this. But this is what I have finally done.
Instead of trying to get the design data system to mock the readonly collection. I've created a new set of sample data just for the collection and made the MainWindow.xaml look at that instead.
So my TestData.xaml file changes to just this. In reality it has more in it but this is just a sample for this question so it looks fairly empty.
<local:MyViewModel
xmlns:local="clr-namespace:ScrapWpfApplication1"
xmlns:system="clr-namespace:System;assembly=mscorlib">
</local:MyViewModel>
Secondly I created a second test data file called TestDataArray.xaml with an array in it. Being sure to set the build action to DesignData.
<x:Array Type="system:String"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="clr-namespace:ScrapWpfApplication1"
xmlns:system="clr-namespace:System;assembly=mscorlib">
<system:String>String 1</system:String>
<system:String>String 2</system:String>
</x:Array>
Finally I changed my MainWindow.xaml file to this. Note the change to the binding on
<Window x:Class="ScrapWpfApplication1.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:local="clr-namespace:ScrapWpfApplication1"
mc:Ignorable="d"
Title="MainWindow" Height="350" Width="525"
d:DataContext="{d:DesignData Source=SampleData.xaml}">
<ListBox ItemsSource="{Binding}" DataContext="{Binding MyCollectionProperty}" d:DataContext="{d:DesignData Source=SampleDataArray.xaml}">
<ListBox.ItemTemplate>
<DataTemplate>
<TextBlock Text="{Binding}" />
</DataTemplate>
</ListBox.ItemTemplate>
</ListBox>
</Window>
This works for my particular scenario, but it would fall down if the sample data was being bound to a control and the ReadOnlyCollection was being read by something inside that control.

Related

Binding data context to view using DesignInstance

In my solution there is one project with 2 folders: ViewModels and Views. ViewModels folder consists of MainViewModel file and 3 folders with other viewmodels, let them be VM1, VM2 and VM3. Views folder consists of MainView file and 3 folders with other views, let them be V1, V2 and V3.
The idea is to put TabControl with 3 tab items with each view on MainView.
I have some problems with namespaces:
VM1:
namespace UPR.ViewModels
{
public class VM1
{
public VM1()
{
}
}
}
V1.xaml.cs:
namespace UPR.Views
{
public partial class V1 : UserControl
{
public V1()
{
InitializeComponent();
}
}
}
V1.xaml:
<UserControl x:Class="UPR.Views.V1"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:ViewModels="clr-namespace:UPR.ViewModels"
mc:Ignorable="d"
d:DesignHeight="450" d:DesignWidth="800"
DataContext="{d:DesignInstance ViewModels:VM1}">
<Grid>
<StackPanel></StackPanel>
</Grid>
MainView.xaml:
<UserControl x:Class="UPR.Views.MainView"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:Views="clr-namespace:UPR.Views"
xmlns:ViewModels="clr-namespace:UPR.ViewModels"
xmlns:dx="http://schemas.devexpress.com/winfx/2008/xaml/core"
mc:Ignorable="d" d:DesignHeight="300" d:DesignWidth="300"
DataContext="{d:DesignInstance ViewModels:MainViewModel}">
<Grid>
<dx:DXTabControl Grid.Row="0">
<dx:DXTabItem x:Name="xV1Tab">
<dx:DXTabItem.Header>
<TextBlock Text="V1 tab" FontSize="14"/>
</dx:DXTabItem.Header>
<Views:MonitoringView DataContext="{Binding V1Content}"/>
</dx:DXTabItem>
<dx:DXTabItem x:Name="xV2Tab">
<dx:DXTabItem.Header>
<TextBlock Text="V2 tab" FontSize="14"/>
</dx:DXTabItem.Header>
<Views:DetailsView DataContext="{Binding V2Content}"/>
</dx:DXTabItem>
<dx:DXTabItem x:Name="xV3Tab">
<dx:DXTabItem.Header>
<TextBlock Text="V3 tab" FontSize="14"/>
</dx:DXTabItem.Header>
<Views:NewTasksView DataContext="{Binding V3Content}"/>
</dx:DXTabItem>
</dx:DXTabControl>
</Grid>
The project doesn't even compile because of two kinds of errors:
1 - VM1 doesn't exist in namespace "clr-namespace:ViewModels"
2 - tag "DesignInstance" doesn't exist in namespace XML "http://schemas.microsoft.com/expression/blend/2008"
These errors are for other views and viewmodels(VM2, VM3, V2, V3, MainVM, MainV), too.
Seems like it's a lag. Because when I write "ViewModels:" - IntelliSense offers me list of viewmodels by that path. When I write "d:" IntelliSense offers me to write "d:DesignInstance".
Any ideas, how to solve this? May be I should do something with namespaces or project structure?
To add to the comment, sometimes one doesn't want the data to load in design time, then use IsDesignTimeCreatable as needed:
d:DataContext="{d:DesignInstance vm:Episode, d:IsDesignTimeCreatable=False}"
Othertimes one may have mock data which one wants to hook up, then setup static data:
d:DataContext="{x:Static local:MockDataSource.VM1}"

Creating a child Window without an MVVM framework

I'm writing a small application whilst learning MVVM in WPF.
As long as I keep on using one Window, everything is pretty easy.
Now I want to open a new Window with a specific ViewModel.
I have a main ViewModel, which contains a Command that should open a new Window / ViewModel, along with a Parameter.
To do this in an MVVM way, I've created a NavigationService, which I'd like to call like this:
public MainWindowViewModel()
{
DetailsCommand = new DelegateCommand(Details);
}
public void Details()
{
SessionsViewModel sessions = new SessionsViewModel();
_NavigationService.CreateWindow(sessions);
}
I've noticed that it's possible to "bind" Views and ViewModels in XAML, like this:
<Application x:Class="TimeTracker.App"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="clr-namespace:TimeTracker"
xmlns:vm="clr-namespace:TimeTracker.ViewModels"
xmlns:vw="clr-namespace:TimeTracker.Views"
StartupUri="Views/MainWindow.xaml">
<Application.Resources>
<ResourceDictionary>
<DataTemplate DataType="{x:Type vm:MainWindowViewModel}">
<vw:MainWindow />
</DataTemplate>
<DataTemplate DataType="{x:Type vm:SessionsViewModel}">
<vw:Sessions />
</DataTemplate>
</ResourceDictionary>
</Application.Resources>
</Application>
<Window x:Class="TimeTracker.Views.Sessions"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:local="clr-namespace:TimeTracker.Views"
xmlns:vm="clr-namespace:TimeTracker.ViewModels"
mc:Ignorable="d"
Title="Sessions" Height="300" Width="300">
<Window.DataContext>
<vm:SessionsViewModel/>
</Window.DataContext>
<Grid>
<TextBlock Text="Hallo" />
</Grid>
</Window>
The problem I'm having is that I don't know how I can use this ResourceDictionary in my NavigationService, so that I can create a new Window by only using its ViewModel.
class NavigationService
{
public void CreateWindow(IViewModel viewModel)
{
//How do I create a new Window using the ResourceDictionary?
}
}
Make sure you have a ContentControl or ContentPresenter in your new window so that the ViewModel can be presented. Next, make sure that resource dictionary is in scope. Putting it in Application.Resources will make it global and guarantee that WPF can find the DataTemplate.
Also, don't use a Window class as your view in the DataTemplate. Use your sub-window panel (e.g., Grid, StackPanel, etc).
I do this:
<blah:MyChildWindow>
<ContentControl Content={Binding DataContext}/>
</blah:MyChildWindow>
And in Application.Resources:
<DataTemplate DataType={x:Type blah:MyViewModel}>
<blah:MyChildWindow/>
</DataTemplate>
BTW - using DataTemplates the way you are trying to do is an excellent pattern.

Custom Markup Extension for Data Context Always Treated as type System.Object

I'm trying to create a custom XAML markup extension that will take a type as an argument, and at runtime, resolve that type using an IoC container, but at design time, simply create it using the default constructor. For now, I'm just trying to implement the default constructor portion. It will look like this:
<UserControl ...
DataContext="{custom:MyCustomExtension MyType}"
<TextBox Text="{Binding SomeProperty}" />
</UserControl>
The issue is that the designer always treats the value my extension produces as type object, so I can't use the GUI binding tools, yet it works fine during runtime.
Here's my very basic implementation to reproduce the issue.
[MarkupExtensionReturnType(typeof(object))]
public class MyCustomExtension : MarkupExtension
{
[ConstructorArgument("dataContextType")]
public Type DataContextType { get; set; }
public MyCustomExtension () { }
public MyCustomExtension (Type dataContextType)
{
DataContextType = dataContextType;
}
public override object ProvideValue(IServiceProvider serviceProvider)
{
return Activator.CreateInstance(DataContextType);
}
}
I've tried using reflector to study how StaticResourceExtension does it, because while it also has the [MarkupExtensionReturnType(typeof(object))] attribute, the VS designer has no issue using the real type of the resource being referenced, but couldn't find anything special using that route.
I tried your custom markup extension in Blend (Microsoft Blend for Visual Studio Professional 2015) and it worked for me:
The change in this "Create Data Binding" window is only visible after a rebuild.
My XAML:
<Window x:Class="DesignTimeTypedDataContext.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:local="clr-namespace:DesignTimeTypedDataContext"
mc:Ignorable="d"
Title="MainWindow" Height="350" Width="525"
DataContext="{local:MyCustom {x:Type local:MainWindowViewModel}}">
<Grid>
<TextBlock x:Name="textBlock" HorizontalAlignment="Left" Margin="14.862,19.706,0,0" TextWrapping="Wrap" Text="{Binding Text}" VerticalAlignment="Top"/>
</Grid>
</Window>
BTW, I guess you know this, but there is d:DataContext which gives you a design time DataContext without affecting the runtime.
<Window x:Class="DesignTimeTypedDataContext.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:local="clr-namespace:DesignTimeTypedDataContext"
mc:Ignorable="d"
Title="MainWindow" Height="350" Width="525"
d:DataContext="{d:DesignInstance {x:Type local:MainWindowViewModel}}">
<Grid>
<TextBlock x:Name="textBlock" HorizontalAlignment="Left" Margin="14.862,19.706,0,0" TextWrapping="Wrap" Text="" VerticalAlignment="Top"/>
</Grid>
</Window>
Maybe this is not much of an answer, just thought I'd share it, maybe it helps in debugging. Now at least you that know your code works for some :)

Problems with binding a static ObservableCollection

I have a class (very little code for testing purposes) which contains a static ObservableCollection, which gets populated from elsewhere:
public class TestClass
{
public static ObservableCollection<int> TestCollection = new ObservableCollection<int>();
}
... and a basic WPF window with a ListBox:
<Window x:Class="app.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Title="MainWindow" Height="350" Width="525">
<Grid>
<ListBox x:Name="list"/>
</Grid>
</Window>
When I tried binding programmatically:
list.ItemsSource = Containers.TestClass.TestCollection;
... it worked just fine. However, when I try to perform binding through XAML:
<Window x:Class="app.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="clr-namespace:app.Containers"
Title="MainWindow" Height="350" Width="525">
<Window.Resources>
<d:TestClass x:Key="dataSource"/>
</Window.Resources>
<Grid>
<ListBox x:Name="list" ItemsSource="{Binding Source={StaticResource dataSource}, Path=TestCollection}"/>
</Grid>
</Window>
... nothing gets displayed.
I've also tried setting DataContext:
<Window.Resources>
<l:LifeEngine x:Key="dataSource"/>
</Window.Resources>
<Window.DataContext>
<Binding Source="{StaticResource dataSource}"/>
</Window.DataContext>
...and using path...
...and nothing gets displayed once again.
Also, not sure if it matters, but when I make my class static I get an error in my XAML code saying:
The type 'TestClass' is abstract and must include an explicit value.
So, that's out of the question.
Any idea how I can bind that ObservableCollection through XAML?
Problem is you are trying to find static property on an instance object.
<ListBox ItemsSource="{Binding Source={StaticResource dataSource},
Path=TestCollection}"/>
Here dataSource is pointing to an instance of TestClass and by binding Path to TestCollection, you are asking to bind to an instance property TestCollection which is static instead. That's why it's not working.
You have to use x:Static markup extension to bind to static properties. (Note you are not creating any instance object of TestClass)
<ListBox ItemsSource="{Binding Source={x:Static d:TestClass.TestCollection}}"/>
Also be informed that static properties binds one time. If you change the instance at runtime that won't be reflected back on UI. In your case you are dealing with ObservableCollection so when you add/delete item from collection it will be refreshed on UI but in case you reinitialize the list that change won't be reflected back on UI. In case you want to update UI, you have to raise StaticPropertyChangedEvent. In case interested, check out my answer here.

Access to Usercontrols in WPF

I'm just starting out with WPF having used WinForms for some time and seem to have fallen at the first hurdle.
I have my main XAMLdefined as
<Window x:Class="FHIRCDALoader.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="clr-namespace:FHIRCDALoader.xaml"
Title="FHIR CDA Loader" Height="350" Width="525"
Icon="Icons/color_swatch.png">
<Window.CommandBindings>
<CommandBinding Command="ApplicationCommands.New"
Executed="NewDocument" />
</Window.CommandBindings>
<DockPanel>
<local:menubar DockPanel.Dock="Top"/>
<local:toolbar DockPanel.Dock="Top"/>
<local:statusbar DockPanel.Dock="Bottom" />
<RichTextBox x:Name="Body"/>
</DockPanel>
</Window>
Note the use of the user controls, one of which is the "statusbar"
<UserControl x:Class="FHIRCDALoader.xaml.statusbar"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
mc:Ignorable="d"
d:DesignHeight="300" d:DesignWidth="300">
<StatusBar >
<StatusBarItem>
<TextBlock x:Name="bbstatusbar" />
</StatusBarItem>
</StatusBar>
</UserControl>
So in MainWindow.xaml.cs I see I can reference RichTextBox named body from the main XAML file. I can't however reference the TextBlock in the UserControl which is named "bbstatusbar".
How do I set the value of the TextBlock from MainWindow.xaml.cs?
In agreement with Vlad and HighCore's comments: you don't set the TextBlock from MainWindow.xaml.cs. You bind it to a view-model. A binding simply looks like this:
<TextBlock Text="{Binding StatusText}" />
The above says: bind the Text property to a property in the current data-context called "StatusText". Next, create a view model:
public class ViewModel : INotifyPropertyChanged
{
public string StatusText
{
get { return _statusText; }
set
{
_statusText = value;
RaisePropertyChanged("StatusText");
}
}
// TODO implement INotifyPropertyChanged
}
Finally, set the DataContext of your MainPage to the view model. You can do this in a variety of ways, but let's say here for simplicity, do it in the constructor:
public MainWindow()
{
InitializeComponent();
DataContext = new ViewModel { StatusText = "hello world" };
}
Now, the idea is to put your model-related logic into ViewModel. So, you shouldn't need to access the UI elements -- instead, update the view-model properties that the UI elements are bound to.

Categories

Resources