Dynamic changing layout in UserControl in ObservableCollection with ComboBox WPF - c#

I have made an UserControl (let's call it Section) where user can change its layout based on value in combobox. In MainView I have ObservableCollection of those UserControls and also implemented command for moving them across this collection. Everything using MVVM pattern. The problem is every time I move an element, my layout is returning to default. In SectionModel's properties everything stays the same as it was, even combobox is displaying correct value.
I am changing layout using code-behind of the view based on value in combobox populated by MainViewModel.
I debug and trace the code starting from clicking "moving" button and it looks like compiler is creating new object of SectionView, but is not regrabing current layout format. So I think I need a way to regrab combobox value and run switch statement in View's code-behind.
PS. After changing format again in broken section everything displaying okay, data from properties were stored. Also I am using Prism if its matter.
MainViewModel
public MainViewModel(IDialogService dialogService)
{
_dialogService = dialogService;
Formats = new ObservableCollection<string>()
{
"Text",
"Photo",
"Text/Photo",
"Photo/Text",
"Photo/Photo"
};
}
private ObservableCollection<SectionModel> _sections;
public ObservableCollection<SectionModel> Sections
{
get { return _sections; }
set { SetProperty(ref _sections, value); }
}
private ObservableCollection<string> _formats;
public ObservableCollection<string> Formats
{
get { return _formats; }
set { SetProperty(ref _formats, value); }
}
private DelegateCommand<SectionModel> _moveSectionDown;
public DelegateCommand<SectionModel> MoveSectionDown =>
_moveSectionDown ?? (_moveSectionDown = new DelegateCommand<SectionModel>(ExecuteMoveDownSection)); /// there is also command for moving up :)
void ExecuteMoveDownSection(SectionModel obj)
{
if (Sections.IndexOf(obj) < Sections.Count() - 1)
{
Sections.Move(Sections.IndexOf(obj), Sections.IndexOf(obj) + 1);
RecalculateSectionsIndexes();
}
}
private void RecalculateSectionsIndexes()
{
foreach (var section in Sections)
section.SectionId = Sections.IndexOf(section) + 1;
}
SectionModel
public class SectionModel : BindableBase
{
[Key]
public int Id { get; set; }
private string _selectedFormat;
public string SelectedFormat
{
get { return _selectedFormat; }
set { SetProperty(ref _selectedFormat, value); }
}
public string Content { get; set; } = string.Empty;
private OfferPhoto? _leftPhoto;
public virtual OfferPhoto? LeftPhoto
{
get { return _leftPhoto; }
set { SetProperty(ref _leftPhoto, value); }
}
public int RightPhotoId { get; set; }
private OfferPhoto? _rightPhoto;
public virtual OfferPhoto? RightPhoto
{
get { return _rightPhoto; }
set { SetProperty(ref _rightPhoto, value); }
}
private int _sectionId;
public int SectionId
{
get { return _sectionId; }
set { SetProperty(ref _sectionId, value); }
}
}
SectionView.xaml // I will simplify it for this post and show only needed controls
<ComboBox x:Name="cbSelectedFormat" MinWidth="130" Margin="0 10 0 10" ItemsSource="{Binding DataContext.Formats, ElementName=offerEditor}"
SelectedItem="{Binding SelectedFormat, UpdateSourceTrigger=PropertyChanged, Mode=TwoWay}" DropDownClosed="cbSelectedFormat_DropDownClosed"/>
<Button Width="{Binding ActualHeight, ElementName=buttonPanel}" BorderThickness="0" Command="{Binding DataContext.MoveSectionDown, ElementName=offerEditor}" CommandParameter="{Binding}">
<fa:ImageAwesome Icon="Solid_ArrowCircleDown" Width="20" Opacity="0.6" Foreground="ForestGreen"/>
</Button>
<Grid x:Name="imageGrid" Grid.Row="1" Background="White">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="50*"/>
<ColumnDefinition Width="50*"/>
</Grid.ColumnDefinitions>
<TextBox x:Name="tbContent" Margin="20"
Padding="5" VerticalAlignment="Stretch" HorizontalAlignment="Stretch"
TextWrapping="WrapWithOverflow" TextAlignment="Justify" FontFamily="Open Sans" FontSize="16"
Text="{Binding Description, UpdateSourceTrigger=PropertyChanged, Mode=TwoWay}" />
<Button x:Name="leftImage" Margin="20"
HorizontalAlignment="Center"
Command="{Binding DataContext.ChooseLeftPhotoCommand, ElementName=offerEditor}" CommandParameter="{Binding}">
<Image>
<Image.Style>
<Style TargetType="Image">
<Setter Property="Source" Value="{Binding LeftPhoto.PhotoUri, UpdateSourceTrigger=PropertyChanged}"/>
<Style.Triggers>
<DataTrigger Binding="{Binding LeftPhoto}" Value="{x:Null}">
<Setter Property="Source" Value="/AppNameAssembly;component/resources/images/placeholder_description_image.jpg"/>
</DataTrigger>
</Style.Triggers>
</Style>
</Image.Style>
</Image>
</Button>
<Button x:Name="rightImage" Margin="20"
HorizontalAlignment="Center"
Command="{Binding DataContext.ChooseRightPhotoCommand, ElementName=offerEditor}" CommandParameter="{Binding}">
<Image>
<Image.Style>
<Style TargetType="Image">
<Setter Property="Source" Value="{Binding RightPhoto.PhotoUri, UpdateSourceTrigger=PropertyChanged}"/>
<Style.Triggers>
<DataTrigger Binding="{Binding RightPhoto}" Value="{x:Null}">
<Setter Property="Source" Value="/AppNameAssembly;component/resources/images/placeholder_description_image.jpg"/>
</DataTrigger>
</Style.Triggers>
</Style>
</Image.Style>
</Image>
</Button>
</Grid>
I use buttons for holding images cause I can change image after clicking it.
SectionView.xaml.cs
public partial class SectionView : UserControl
{
public SectionView()
{
InitializeComponent();
}
public string SelectedFormat { get; private set; } // This property is only for testing if view stored SelectedFormat - no it did not. After moving UserControl across the list SelectedFormat here is null, but in SectionModel is set correct.
private void cbSelectedFormat_DropDownClosed(object sender, System.EventArgs e)
{
SelectedFormat = cbSelectedFormat.SelectedItem.ToString();
if (SelectedFormat != null)
switch (SelectedFormat)
{
case "Photo":
leftImage.Visibility = Visibility.Visible;
Grid.SetColumn(leftImage, 0);
Grid.SetColumnSpan(leftImage, 2);
tbContent.Visibility = Visibility.Collapsed;
rightImage.Visibility = Visibility.Collapsed;
break;
case "Text":
leftImage.Visibility = Visibility.Collapsed;
tbContent.Visibility = Visibility.Visible;
Grid.SetColumn(tbContent, 0);
Grid.SetColumnSpan(tbContent, 2);
rightImage.Visibility = Visibility.Collapsed;
break;
case "Text/Photo":
leftImage.Visibility = Visibility.Visible;
Grid.SetColumn(leftImage, 1);
Grid.SetColumnSpan(leftImage, 1);
tbContent.Visibility = Visibility.Visible;
Grid.SetColumn(tbContent, 0);
Grid.SetColumnSpan(tbContent, 1);
rightImage.Visibility = Visibility.Collapsed;
break;
case "Photo/Text":
leftImage.Visibility = Visibility.Visible;
Grid.SetColumn(leftImage, 0);
Grid.SetColumnSpan(leftImage, 1);
tbContent.Visibility = Visibility.Visible;
Grid.SetColumn(tbContent, 1);
Grid.SetColumnSpan(tbContent, 1);
rightImage.Visibility = Visibility.Collapsed;
break;
case "Photo/Photo":
leftImage.Visibility = Visibility.Visible;
Grid.SetColumn(leftImage, 0);
Grid.SetColumnSpan(leftImage, 1);
tbContent.Visibility = Visibility.Collapsed;
rightImage.Visibility = Visibility.Visible;
Grid.SetColumn(rightImage, 1);
Grid.SetColumnSpan(rightImage, 1);
break;
}
}
}
MainView (offerEditor)
<ListBox Height="640" ItemsSource="{Binding Sections, UpdateSourceTrigger=PropertyChanged}" ScrollViewer.CanContentScroll="False" ScrollViewer.HorizontalScrollBarVisibility="Hidden" ScrollViewer.VerticalScrollBarVisibility="Auto">
<ListBox.ItemContainerStyle>
<Style TargetType="ListBoxItem">
<Setter Property="Padding" Value="0"/>
<Setter Property="Width" Value="auto"/>
</Style>
</ListBox.ItemContainerStyle>
<ListBox.ItemTemplate>
<DataTemplate>
<uc:SectionView MaxWidth="{Binding ActualWidth, ElementName=sectionsMenuBar}" DataContext="{Binding}" ></uc:SectionView>
</DataTemplate>
</ListBox.ItemTemplate>
</ListBox>
I can't find any information about ObservableCollection<>.Move(int oldIndex, int newIndex,) method, but I suspect it might be it. Maybe this method is copying object, deleting old one and creating new one based on copy on new index in collection, I don't know.
Does somebody know what it could be or had similar problem? Thanks for help!

Related

How to properly bind multiple ViewModels when using DataTemplateSelector [duplicate]

This question already has answers here:
How to use DataTemplateSelector with ContentControl to display different controls based on the view-model?
(2 answers)
Closed 4 years ago.
I'm trying to write a simple dialog that would accept a value in a SpinEdit or a text in a TextEdit. I'm using multiple VMs and I made a selector that should view a proper control based on the logic in the c++/cli file.
XAML:
xmlns:local="clr-namespace:asd"
Title="{Binding Path=Title, Mode=OneTime}"
<dx:DXWindow.Resources>
<DataTemplate x:Key="TInputValueVM" DataType="{x:Type local:TInputValueVM}">
<dxe:SpinEdit Height="23" Width="200"
Text="{Binding Value, Mode=TwoWay}"
Mask="{Binding Mask, Mode=OneWay}"
MaxLength="{Binding Path=InputLength}" />
</DataTemplate>
<DataTemplate x:Key="TInputTextVM" DataType="{x:Type local:TInputTextVM}">
<dxe:TextEdit Height="23" Width="200"
Text="{Binding Value, Mode=TwoWay}"
MaskType="RegEx" Mask="{Binding Mask, Mode=OneWay}"
MaxLength="{Binding Path=InputLength}"/>
</DataTemplate>
<local:PropertyDataTemplateSelector x:Key="templateSelector"
DataTemplate_Value="{StaticResource TInputValueVM}"
DataTemplate_Text="{StaticResource TInputTextVM}" />
</dx:DXWindow.Resources>
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
</Grid.RowDefinitions>
<StackPanel Grid.Row="0" >
<Label x:Uid="Label" MinHeight="24" MinWidth="60" Content="Value" />
<ContentControl Content="{Binding Path=Whoami}" ContentTemplateSelector="{StaticResource templateSelector}" />
</StackPanel>
<StackPanel Grid.Row="1" x:Uid="OKCancel_Buttons" Orientation="Horizontal" HorizontalAlignment="Right" VerticalAlignment="Bottom">
<Button Height="23" x:Name="OK_Button" Click="OK_Click" Content="OK" IsDefault="True" HorizontalAlignment="Right" MinWidth="95" />
<Button Height="23" x:Name="Cancel_Button" Click="Cancel_Click" Content="Cancel" HorizontalAlignment="Right" MinWidth="95" />
</StackPanel>
</Grid>
In c# I have a base VM and two VMS that extend it, one for values and one for text. The rest of the properties stay the same.
C#
namespace asd
{
public class TInputBaseVM : ViewModelBase
{
private string m_sTitle;
private string m_sMask;
private int m_nInputLenght;
private string m_sWhoami;
public TInputBaseVM(string A_sTitle, string A_sMask, int A_nInputLength)
{
m_sTitle = A_sTitle;
m_sMask = A_sMask;
m_nInputLenght = A_nInputLength;
}
protected string Title
{
get { return m_sTitle; }
set { SetProperty(ref m_sTitle, value, () => Title); }
}
protected string Mask
{
get { return m_sMask; }
set { SetProperty(ref m_sMask, value, () => Mask); }
}
protected int InputLength
{
get { return m_nInputLenght; }
set { SetProperty(ref m_nInputLenght, value, () => InputLength); }
}
protected string Whoami
{
get { return m_sWhoami; }
set { SetProperty(ref m_sWhoami, value, () => Whoami); }
}
}
public class TInputValueVM : TInputBaseVM
{
public TInputValueVM(string A_sTitle, string A_sMask, int A_nInputLength, double A_nValue) : base(A_sTitle, A_sMask, A_nInputLength)
{
Value = A_nValue;
Whoami = "Value";
}
private double m_nValue;
public double Value
{
get { return m_nValue; }
set { SetProperty(ref m_nValue, value, () => Value); }
}
}
public class TInputTextVM : TInputBaseVM
{
public TInputTextVM(string A_sTitle, string A_sMask, int A_nInputLength, string A_sValue) : base(A_sTitle, A_sMask, A_nInputLength)
{
Value = A_sValue;
Whoami = "Text";
}
private string m_sValue;
public string Value
{
get { return m_sValue; }
set { SetProperty(ref m_sValue, value, () => Value); }
}
}
public class PropertyDataTemplateSelector : DataTemplateSelector
{
public DataTemplate DataTemplate_Value { get; set; }
public DataTemplate DataTemplate_Text { get; set; }
public override DataTemplate SelectTemplate(object item, DependencyObject container)
{
var selector = item as string;
if(selector == "Value")
return DataTemplate_Value;
return DataTemplate_Text;
}
}
}
In c++/cli I create an object of a proper VM and I'd like the WPF to automatically update the view to either spinedit or textedit, however I'm not sure how to properly bind the properties from the C#. If I explicitly type 'Value' in the Content property of the ContentControl then it displays the spinEdit but I don't know how to bind it so it automatically takes the correct property.
EDIT: I'm adding c++/cli code to show how I choose different VMs
C++/cli:
bool TSignalNumberPositionDialogCLR::StartDialog(TSignalNumberPositionSupport& A_Attributes, HWND A_hwndParent, LPTSTR String)
{
try
{
TInputValueVM ^oExchange_Value;
TInputTextVM ^oExchange_Text;
int inputFormat = A_Attributes.GetInputFormat();
if(inputFormat)
oExchange_Text = gcnew TInputTextVM(gcnew System::String(A_Attributes.GetTitle()), gcnew System::String(A_Attributes.GetMask()),
A_Attributes.GetInputLength(), gcnew System::String(A_Attributes.GetInitialText()));
else
oExchange_Value = gcnew TInputValueVM(gcnew System::String(A_Attributes.GetTitle()), gcnew System::String(A_Attributes.GetMask()),
A_Attributes.GetInputLength(), A_Attributes.GetInitialValue());
Dialogs::TSignalNumberPositionDialog^ dialog = gcnew Dialogs::TSignalNumberPositionDialog();
if(inputFormat)
dialog->DataContext = oExchange_Text;
else
dialog->DataContext = oExchange_Value;
dialog->ShowDialog();
if(dialog->DialogResult)
{
CString nValue;
if(inputFormat)
nValue = oExchange_Text->Value;
else
nValue = ((Decimal)oExchange_Value->Value).ToString("F2", CultureInfo::InvariantCulture);
A_Attributes.UpdateValue(nValue, String, A_Attributes.GetInputLength());
return true;
}
return false;
}
catch(Exception^ e)
{
e;
}
}
based on the 'inputFormat' variable I want to display different controls in the dialog.
EDIT: Based on #Clemens comments I got rid of the selector sectionand the x:Key property in the DataTemplates. I changed the content opf the Content property to Content="{Binding}" and it somehow works. The moment I create a VM it selects the correct one.
Based on your comment. Let me improve my answer. As you are facing issue in VM selection. so plesae concentrate how I assigned VM to datatemplate. Although it is done in very basic way, you can handle it if you you are using MVVM packages.
I have created 2 data template and 2 vms and each vm is bound to datatemplate. To verify, I have a combobox, which will select datatemplate based on selected value.
Here is Sample VM
public class VM : System.ComponentModel.INotifyPropertyChanged
{
private string title;
private SolidColorBrush background;
public string Title { get => title; set { title = value; RaisePropertyChanged(); } }
public SolidColorBrush Background { get => background; set { background = value; RaisePropertyChanged(); } }
public event PropertyChangedEventHandler PropertyChanged;
private void RaisePropertyChanged([System.Runtime.CompilerServices.CallerMemberName] string name = "")
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(name));
}
}
public class VM1: VM
{
public VM1()
{
Title = "This is VM1";
Background = Brushes.Yellow;
}
}
public class VM2: VM
{
public VM2()
{
Title = "This is VM2";
Background = Brushes.Orange;
}
}
Now check for resources
<local:VM1 x:Key="VM1"/>
<local:VM2 x:Key="VM2"/>
<DataTemplate x:Key="DT1">
<Grid DataContext="{StaticResource VM1}">
<TextBlock Text="{Binding Title}" Background="{Binding Background}"/>
</Grid>
</DataTemplate>
<DataTemplate x:Key="DT2">
<Grid DataContext="{StaticResource VM2}">
<TextBlock Text="{Binding Title}" Background="{Binding Background}"/>
</Grid>
</DataTemplate>
<Style TargetType="ContentControl" x:Key="contentStyle">
<Style.Triggers>
<DataTrigger Binding="{Binding ElementName=cmbo, Path=SelectedValue}" Value="Template1">
<Setter Property="ContentTemplate" Value="{StaticResource DT1}" />
</DataTrigger>
<DataTrigger Binding="{Binding ElementName=cmbo, Path=SelectedValue}" Value="Template2">
<Setter Property="ContentTemplate" Value="{StaticResource DT2}" />
</DataTrigger>
</Style.Triggers>
</Style>
and finally I have combobox and content control just to verify
<ComboBox Name="cmbo"/>
<ContentControl Style="{StaticResource contentStyle}"/>
where cmbo.ItemsSource = new List { "Template1", "Template2" };
Hope you got the point

How to Bind ItemsSource of ComboBox in ControlTemplate?

Situation
I have to change the content of the Flyout Item in GridView. So I am creating ControlTemplate in Page.Resources and setting it in ContentControl which is inside Flyout.
Problem
I have a ComboBox in ControlTemplate. Now I want to set the ItemsSource of ComboBox to List<string> (_easingType) which is declared in MainPage
Question
How to Bind/Set ItemsSource of ComboBox in ControlTemplate?
Code
I have removed the unnecessary parts of the code
XAML
<Page.Resources>
<ControlTemplate x:Key="BlurEditFlyout">
....
<ComboBox ItemsSource="{Bind it to the _esaingType}" />
....
<ControlTemplate x:Key="BlurEditFlyout">
</Page.Resources>
<GridView ItemsSource="{x:Bind _items}">
<GridView.ItemTemplate>
<DataTemplate x:DataType="local:MethodData">
<StackPanel>
....
<Button Visibility="{x:Bind EditButtonVisibility}">
<Button.Flyout>
<Flyout>
<Flyout.FlyoutPresenterStyle>
<Style TargetType="FlyoutPresenter">
<Setter Property="ScrollViewer.HorizontalScrollMode" Value="Disabled" />
<Setter Property="ScrollViewer.HorizontalScrollBarVisibility" Value="Disabled" />
</Style>
</Flyout.FlyoutPresenterStyle>
<ContentControl Template="{x:Bind FlyoutTemplate}"/>
</Flyout>
</Button.Flyout>
<SymbolIcon Symbol="Edit"/>
</Button>
....
</StackPanel>
</DataTemplate>
</GridView.ItemTemplate>
</GridView>
Code Behind
public sealed partial class MainPage : Page
{
ObservableCollection<MethodData> _items = new ObservableCollection<MethodData>();
List<string> _easingType = new List<string>(Enum.GetNames(typeof(EasingType)).ToArray());
Dictionary<MethodName, ControlTemplate> _buttonFlyoutDictionary = new Dictionary<MethodName, ControlTemplate>();
public MainPage()
{
this.InitializeComponent();
LoadFlyoutResources();
_items.Add(GetMethodData(MethodName.Blur));
}
private void LoadFlyoutResources()
{
_buttonFlyoutDictionary.Add(MethodName.Blur, (ControlTemplate)Resources["BlurEditFlyout"]);
.....
}
private MethodData GetMethodData(MethodName methodName)
{
_buttonFlyoutDictionary.TryGetValue(methodName, out ControlTemplate flyoutTemplate);
return new MethodData(methodName, flyoutTemplate);
}
}
public class MethodData
{
public string Name { get; set; }
public ControlTemplate FlyoutTemplate { get; set; }
public Visibility EditButtonVisibility { get; set; }
public MethodData(MethodName name, ControlTemplate flyoutTemplate)
{
Name = name.ToString();
FlyoutTemplate = flyoutTemplate;
EditButtonVisibility = (name == MethodName.Then) ? Visibility.Collapsed : Visibility.Visible;
}
}
public enum MethodName
{
Blur,
....
}
Full Code
AnimationSetSamplePage.zip
Your DataContext in the Flyout control is actually each item in "_items". You'll want to create a DataContext proxy to get to your Page's DataContext. You can follow either of these two links to create the proxy.
https://weblogs.asp.net/dwahlin/creating-a-silverlight-datacontext-proxy-to-simplify-data-binding-in-nested-controls
http://www.thomaslevesque.com/2011/03/21/wpf-how-to-bind-to-data-when-the-datacontext-is-not-inherited/
The gist of it is you'll want to create a proxy that you can reference as a static resource. Following the first link, you'll do something like this:
public class DataContextProxy : FrameworkElement
{
public DataContextProxy()
{
this.Loaded += new RoutedEventHandler(DataContextProxy_Loaded);
}
void DataContextProxy_Loaded(object sender, RoutedEventArgs e)
{
Binding binding = new Binding();
if (!String.IsNullOrEmpty(BindingPropertyName))
{
binding.Path = new PropertyPath(BindingPropertyName);
}
binding.Source = this.DataContext;
binding.Mode = BindingMode;
this.SetBinding(DataContextProxy.DataSourceProperty, binding);
}
public Object DataSource
{
get { return (Object)GetValue(DataSourceProperty); }
set { SetValue(DataSourceProperty, value); }
}
public static readonly DependencyProperty DataSourceProperty =
DependencyProperty.Register("DataSource", typeof(Object), typeof(DataContextProxy), null);
public string BindingPropertyName { get; set; }
public BindingMode BindingMode { get; set; }
}
You should use public access modifier for _easingType
public List<string> _easingType = new List<string>(Enum.GetNames(typeof(EasingType)).ToArray());
In MainPage.xaml
<Page.Resources>
<local:DataContextProxy x:Key="DataContextProxy" />
<ControlTemplate x:Key="BlurEditFlyout">
....
<ComboBox ItemsSource="{Binding Source={StaticResource DataContextProxy}, Path=DataSource._easingType}" />
....
<ControlTemplate x:Key="BlurEditFlyout">
</Page.Resources>
...
How to Bind/Set ItemsSource of ComboBox in ControlTemplate?
I'm not sure if you have deep reasons for asking this question, but directly to reply this question, we could just set the string list _esaingType as the value of DataContext property and binding it. For example:
XAML
<Page.Resources>
<ControlTemplate TargetType="FlyoutPresenter" x:Key="BlurEditFlyout" >
...
<ComboBox ItemsSource="{Binding}" />
...
</ControlTemplate>
</Page.Resources>
<Grid Background="{ThemeResource ApplicationPageBackgroundThemeBrush}">
<Button>
<Button.Flyout>
<Flyout>
<Flyout.FlyoutPresenterStyle>
<Style TargetType="FlyoutPresenter">
<Setter Property="ScrollViewer.HorizontalScrollMode" Value="Disabled" />
<Setter Property="ScrollViewer.HorizontalScrollBarVisibility" Value="Disabled" />
<Setter Property="Template" Value="{StaticResource BlurEditFlyout}">
</Setter>
</Style>
</Flyout.FlyoutPresenterStyle>
<!--<ContentControl Template="{StaticResource BlurEditFlyout}"/>-->
</Flyout>
</Button.Flyout>
<SymbolIcon Symbol="Edit"/>
</Button>
</Grid>
Code behind
List<string> _easingType = new List<string>();
public MainPage()
{
this.InitializeComponent();
_easingType.Add("test2");
_easingType.Add("test1");
this.DataContext = _easingType;
}
Any concerns on this way or any issues when using this on your side please kindly tell me I will follow up in time. More details please reference Data binding in depth.

Binding in DataTrigger seems not working

I have a ComboBox which should have a text when nothing is selected. This seems like a straight forward problem, and has many answers on the net, but unfortunately, it is not working for me. I think, the reason is, that I don't want to show a static text, but rather a bound text.
My minimal not working example looks like this:
public class Model
{
public string Name { get; set; }
public SubModel SelectedItem { get; set; }
public List<SubModel> Items { get; set; }
}
public class SubModel
{
public string Description { get; set; }
}
and the MainWindow:
public partial class MainWindow : Window
{
public MainWindow()
{
InitializeComponent();
var selectedSubModel = new SubModel { Description = "SubModel5" };
var model1 = new Model
{
Name = "Model1",
Items = new List<SubModel>
{
new SubModel { Description = "SubModel1" },
new SubModel { Description = "SubModel2" },
new SubModel { Description = "SubModel3" }
}
};
var model2 = new Model
{
Name = "Model2",
SelectedItem = selectedSubModel,
Items = new List<SubModel>
{
new SubModel { Description = "SubModel4" },
selectedSubModel,
new SubModel { Description = "SubModel6" }
}
};
var model3 = new Model
{
Name = "Model3",
Items = new List<SubModel>
{
new SubModel { Description = "SubModel7" },
new SubModel { Description = "SubModel8" },
new SubModel { Description = "SubModel9" }
}
};
_itemsControl.Items.Add(model1);
_itemsControl.Items.Add(model2);
_itemsControl.Items.Add(model3);
}
}
with xaml:
<Window x:Class="WpfApplication1.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:WpfApplication1="clr-namespace:WpfApplication1"
Title="MainWindow" Height="350" Width="525">
<ItemsControl x:Name="_itemsControl">
<ItemsControl.ItemTemplate>
<DataTemplate DataType="WpfApplication1:Model">
<ComboBox ItemsSource="{Binding Items}"
SelectedItem="{Binding SelectedItem}">
<ComboBox.ItemTemplate>
<DataTemplate>
<TextBlock Text="{Binding Description}"></TextBlock>
</DataTemplate>
</ComboBox.ItemTemplate>
<ComboBox.Style>
<Style TargetType="ComboBox">
<Style.Triggers>
<DataTrigger Binding="{Binding SelectedItem}" Value="{x:Null}">
<Setter Property="Background">
<Setter.Value>
<VisualBrush>
<VisualBrush.Visual>
<TextBlock Text="{Binding Name}"/>
</VisualBrush.Visual>
</VisualBrush>
</Setter.Value>
</Setter>
</DataTrigger>
</Style.Triggers>
</Style>
</ComboBox.Style>
</ComboBox>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
</Window>
This gives the follwing:
But it should look similar to this:
please first of all take in your mind facts provided in the next sentence - you can only select items provided by ComboBox ItemsSource. Thus since the Name property values (Model1, Model2, Model3 etc.) are not in your collection they can't be selected you will see the empty selection Instead. I can suggest you the next solution the combination of data context proxy and wpf behavior.
Xaml code
<Window x:Class="ComboBoxWhenNoAnySelectedHelpAttempt.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:comboBoxWhenNoAnySelectedHelpAttempt="clr-namespace:ComboBoxWhenNoAnySelectedHelpAttempt"
xmlns:system="clr-namespace:System;assembly=mscorlib"
xmlns:i="http://schemas.microsoft.com/expression/2010/interactivity"
Title="MainWindow" Height="350" Width="525">
<Grid>
<ItemsControl x:Name="_itemsControl">
<ItemsControl.ItemTemplate>
<DataTemplate DataType="comboBoxWhenNoAnySelectedHelpAttempt:Model">
<ComboBox x:Name="ComboBox"
SelectedItem="{Binding SelectedItem, UpdateSourceTrigger=PropertyChanged}">
<ComboBox.Resources>
<!--the next object is a proxy that able to provide combo data context each time it requested-->
<comboBoxWhenNoAnySelectedHelpAttempt:FreezableProxyClass x:Key="FreezableProxyClass" ProxiedDataContext="{Binding ElementName=ComboBox, Path=DataContext }"></comboBoxWhenNoAnySelectedHelpAttempt:FreezableProxyClass>
</ComboBox.Resources>
<ComboBox.ItemsSource>
<CompositeCollection>
<!--the next object is a collapsed combo box that can be selected in code-->
<!--keep im mind, since this object is not a SubModel we get the binding expression in output window-->
<ComboBoxItem IsEnabled="False" Visibility="Collapsed" Foreground="Black" Content="{Binding Source={StaticResource FreezableProxyClass},
Path=ProxiedDataContext.Name, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}"></ComboBoxItem>
<CollectionContainer Collection="{Binding Source={StaticResource FreezableProxyClass},
Path=ProxiedDataContext.Items, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}"/>
</CompositeCollection>
</ComboBox.ItemsSource>
<ComboBox.ItemTemplate>
<DataTemplate>
<TextBlock Text="{Binding Description}"></TextBlock>
</DataTemplate>
</ComboBox.ItemTemplate>
<i:Interaction.Behaviors>
<!--next behavior helps to select a zero index (Model.Name collapsed) item from source when selected item is not SubModel-->
<comboBoxWhenNoAnySelectedHelpAttempt:ComboBoxLoadingBehavior/>
</i:Interaction.Behaviors>
</ComboBox>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
</Grid>
Code Behind with Proxy Code
public class FreezableProxyClass : Freezable
{
protected override Freezable CreateInstanceCore()
{
return new FreezableProxyClass();
}
public static readonly DependencyProperty ProxiedDataContextProperty = DependencyProperty.Register(
"ProxiedDataContext", typeof(object), typeof(FreezableProxyClass), new PropertyMetadata(default(object)));
public object ProxiedDataContext
{
get { return (object)GetValue(ProxiedDataContextProperty); }
set { SetValue(ProxiedDataContextProperty, value); }
}
}
/// <summary>
/// Interaction logic for MainWindow.xaml
/// </summary>
public partial class MainWindow : Window
{
public MainWindow()
{
InitializeComponent();
var selectedSubModel = new SubModel { Description = "SubModel5" };
var model1 = new Model
{
Name = "Model1",
Items = new ObservableCollection<SubModel>
{
new SubModel { Description = "SubModel1" },
new SubModel { Description = "SubModel2" },
new SubModel { Description = "SubModel3" }
}
};
var model2 = new Model
{
Name = "Model2",
SelectedItem = selectedSubModel,
Items = new ObservableCollection<SubModel>
{
new SubModel { Description = "SubModel4" },
selectedSubModel,
new SubModel { Description = "SubModel6" }
}
};
var model3 = new Model
{
Name = "Model3",
Items = new ObservableCollection<SubModel>
{
new SubModel { Description = "SubModel7" },
new SubModel { Description = "SubModel8" },
new SubModel { Description = "SubModel9" }
}
};
_itemsControl.Items.Add(model1);
_itemsControl.Items.Add(model2);
_itemsControl.Items.Add(model3);
}
}
public class Model:BaseObservableObject
{
private string _name;
private SubModel _selectedItem;
private ObservableCollection<SubModel> _items;
public string Name
{
get { return _name; }
set
{
_name = value;
OnPropertyChanged();
}
}
public SubModel SelectedItem
{
get { return _selectedItem; }
set
{
_selectedItem = value;
OnPropertyChanged();
}
}
public ObservableCollection<SubModel> Items
{
get { return _items; }
set
{
_items = value;
OnPropertyChanged();
}
}
}
public class SubModel:BaseObservableObject
{
private string _description;
public string Description
{
get { return _description; }
set
{
_description = value;
OnPropertyChanged();
}
}
}
BaseObservableObject code (simple implementation for INotifyPropertyChanged )
/// <summary>
/// implements the INotifyPropertyChanged (.net 4.5)
/// </summary>
public class BaseObservableObject : INotifyPropertyChanged
{
public event PropertyChangedEventHandler PropertyChanged;
protected virtual void OnPropertyChanged([CallerMemberName] string propertyName = null)
{
var handler = PropertyChanged;
if (handler != null) handler(this, new PropertyChangedEventArgs(propertyName));
}
protected virtual void OnPropertyChanged<T>(Expression<Func<T>> raiser)
{
var propName = ((MemberExpression)raiser.Body).Member.Name;
OnPropertyChanged(propName);
}
protected bool Set<T>(ref T field, T value, [CallerMemberName] string name = null)
{
if (!EqualityComparer<T>.Default.Equals(field, value))
{
field = value;
OnPropertyChanged(name);
return true;
}
return false;
}
}
WPF Behavior Code
public class ComboBoxLoadingBehavior:Behavior<ComboBox>
{
private bool _unLoaded;
protected override void OnAttached()
{
base.OnAttached();
AssociatedObject.Loaded += AssociatedObjectOnLoaded;
AssociatedObject.LayoutUpdated += AssociatedObjectOnLayoutUpdated;
AssociatedObject.Unloaded += AssociatedObjectOnUnloaded;
}
private void AssociatedObjectOnUnloaded(object sender, RoutedEventArgs routedEventArgs)
{
_unLoaded = true;
UnsubscribeAll();
}
private void UnsubscribeAll()
{
AssociatedObject.Loaded -= AssociatedObjectOnLoaded;
AssociatedObject.LayoutUpdated -= AssociatedObjectOnLayoutUpdated;
AssociatedObject.Unloaded -= AssociatedObjectOnUnloaded;
}
private void AssociatedObjectOnLayoutUpdated(object sender, EventArgs eventArgs)
{
UpdateSelectionState(sender);
}
private static void UpdateSelectionState(object sender)
{
var combo = sender as ComboBox;
if (combo == null) return;
var selectedItem = combo.SelectedItem as SubModel;
if (selectedItem == null)
{
combo.SelectedIndex = 0;
}
}
private void AssociatedObjectOnLoaded(object sender, RoutedEventArgs routedEventArgs)
{
_unLoaded = false;
UpdateSelectionState(sender);
}
protected override void OnDetaching()
{
base.OnDetaching();
if(_unLoaded) return;
UnsubscribeAll();
}
}
This is a working complete solution for you problem, just copy/past and use it as a starting point for your farther research. I'll glad to help if you will have any problems with the code.
Regards.
I've found 2 possible solutions:
Change ComboBox template
Edit standard combobox template by right button click on combobox in designer and select Edit Template -> Edit a Copy...
After that change ContentPresenter with a custom converter:
XAML
<ContentPresenter ContentTemplate="{TemplateBinding SelectionBoxItemTemplate}" ContentTemplateSelector="{TemplateBinding ItemTemplateSelector}" ContentStringFormat="{TemplateBinding SelectionBoxItemStringFormat}" HorizontalAlignment="{TemplateBinding HorizontalContentAlignment}" IsHitTestVisible="false" Margin="{TemplateBinding Padding}" SnapsToDevicePixels="{TemplateBinding SnapsToDevicePixels}" VerticalAlignment="{TemplateBinding VerticalContentAlignment}">
<ContentPresenter.Content>
<MultiBinding Converter="{local:ComboboxEmptyValueConverter}">
<Binding Path="SelectionBoxItem" RelativeSource="{RelativeSource Mode=TemplatedParent}" />
<Binding Mode="OneWay" Path="DataContext" RelativeSource="{RelativeSource Mode=TemplatedParent}" />
</MultiBinding>
</ContentPresenter.Content>
</ContentPresenter>
C#
class ComboboxEmptyValueConverterExtension : MarkupExtension, IMultiValueConverter
{
public object Convert(object[] values, Type targetType, object parameter, CultureInfo culture)
{
string stringValue = values[0] as string;
var dataContext = values[1] as Model;
return (stringValue != null && String.IsNullOrEmpty(stringValue)) ? dataContext?.Name : values[0];
}
public object[] ConvertBack(object value, Type[] targetTypes, object parameter, CultureInfo culture)
{
return new object[] { value, null };
}
public override object ProvideValue(IServiceProvider serviceProvider)
{
return this;
}
}
Set ComboBox to IsEditable & IsReadOnly and change Text
<ComboBox ItemsSource="{Binding Items}" SelectedItem="{Binding SelectedItem}">
<ComboBox.ItemTemplate>
<DataTemplate>
<TextBlock x:Name="textBlock" Text="{Binding Description}"/>
</DataTemplate>
</ComboBox.ItemTemplate>
<ComboBox.Style>
<Style TargetType="ComboBox">
<Style.Triggers>
<DataTrigger Binding="{Binding SelectedItem}" Value="{x:Null}">
<Setter Property="IsEditable" Value="True" />
<Setter Property="IsReadOnly" Value="True" />
<Setter Property="Text" Value="{Binding Name}" />
</DataTrigger>
</Style.Triggers>
</Style>
</ComboBox.Style>
</ComboBox>
The answer is, to put the visual brush in the resources of the combobox:
<DataTemplate DataType="WpfApplication1:Model">
<ComboBox ItemsSource="{Binding Items}"
SelectedItem="{Binding SelectedItem}">
<ComboBox.ItemTemplate>
<DataTemplate>
<TextBlock Text="{Binding Description}"></TextBlock>
</DataTemplate>
</ComboBox.ItemTemplate>
<ComboBox.Resources>
<VisualBrush x:Key="_myBrush">
<VisualBrush.Visual>
<TextBlock Text="{Binding Name}"/>
</VisualBrush.Visual>
</VisualBrush>
</ComboBox.Resources>
<ComboBox.Style>
<Style TargetType="ComboBox">
<Style.Triggers>
<DataTrigger Binding="{Binding SelectedItem}" Value="{x:Null}">
<Setter Property="Background" Value="{StaticResource _myBrush}"/>
</DataTrigger>
</Style.Triggers>
</Style>
</ComboBox.Style>
</ComboBox>
</DataTemplate>
Then, together with the rest of the code, it works like expected.

CollectionView only sorting first time (when create viewmodel again it doesnt)

I have problems to understand the CollectionView. I implemented it with some sorting/grouping. But it is only sorting the first time the view is created.
View:
<DataGrid ItemsSource="{Binding varCollectionview}" Grid.Row="2" AutoGenerateColumns="False" SelectedItem="{Binding selVariable}" Grid.ColumnSpan="2">
<DataGrid.GroupStyle>
<GroupStyle>
<GroupStyle.ContainerStyle>
<Style TargetType="{x:Type GroupItem}">
<Setter Property="Template">
<Setter.Value>
<ControlTemplate>
<StackPanel Orientation="Vertical">
<TextBlock Text="{Binding Name.value}" Foreground="White" Background="Gray" HorizontalAlignment="Stretch" TextAlignment="Left" Padding="10,0" />
<ItemsPresenter></ItemsPresenter>
</StackPanel>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
</GroupStyle.ContainerStyle></GroupStyle>
</DataGrid.GroupStyle>
<i:Interaction.Behaviors>
<behavior:DataGridScrollIntoViewBehavior />
</i:Interaction.Behaviors>
<DataGrid.Columns>
...
...
the MainViewModel call:
variablenVm = new VariablenViewModel(cfg);
it is called everytime i load other values etc. for reloading the Tab.
the actual ViewModel for the TabItem:
public class VariablenViewModel : ViewModelBase
{
private string _filterString;
public string filterString
{
get { return _filterString ; }
set
{
_filterString = value.ToLower();
RaisePropertyChanged("filterString ");
varCollectionview.Refresh();
}
}
private SimuVariable _selVariable;
public SimuVariable selVariable
{
get { return _selVariable; }
set
{
_selVariable = value;
RaisePropertyChanged("selVariable");
}
}
private Konfiguration _cfg;
public Konfiguration cfg
{
get { return _cfg; }
set
{
_cfg = value;
RaisePropertyChanged("cfg");
}
}
private ICollectionView _varCollectionview;
public ICollectionView varCollectionview
{
get { return _varCollectionview; }
set
{
_varCollectionview = value;
RaisePropertyChanged("varCollectionview");
}
}
public VariablenViewModel(Konfiguration _mainCfg)
{
cfg = _mainCfg;
hasSomeValueChanged = false;
_filterString = "";
_neueVariableCommand = new RelayCommand(() =>
{
...
});
_loescheVariableCommand = new RelayCommand(
...
);
varCollectionview = (CollectionView)CollectionViewSource.GetDefaultView(cfg.variablen);
if (varCollectionview.GroupDescriptions.Count == 0)
{
varCollectionview.GroupDescriptions.Add(new PropertyGroupDescription("varType"));
}
if (varCollectionview.SortDescriptions.Count == 0)
{
varCollectionview.SortDescriptions.Add(new SortDescription("varType", ListSortDirection.Descending));
varCollectionview.SortDescriptions.Add(new SortDescription("name", ListSortDirection.Ascending));
}
varCollectionview.Filter = new Predicate<object>(filter);
varCollectionview.Refresh();
}
...
}
edit: When i first look at the view, the collection is sorted. If i create a new Viewmodel after that, sorting doesnt happen.
After some Debugging i found out that the Sortdescriptions get lost. In the Constructor i add 2 Sortdescription and 1 groupdescription. Some time later i have only the groupdescription left and the sorts are gone and i don't know why :(
http://imgur.com/U7362YP
-somewhere the SortDescriptions are lost/deleted
Another question, is it possible to add a SortDescription like varType.id?
Hell yeah, i found a Solution.
varCollectionview = new CollectionViewSource();
varCollectionview.Source = cfg.variablen;
Dispatcher.CurrentDispatcher.BeginInvoke(DispatcherPriority.Background, new Action(delegate()
{
varCollectionview.View.SortDescriptions.Add(new SortDescription("varType", ListSortDirection.Descending));
varCollectionview.View.SortDescriptions.Add(new SortDescription("name", ListSortDirection.Ascending));
varCollectionview.View.GroupDescriptions.Add(new PropertyGroupDescription("varType"));
varCollectionview.View.Filter = new Predicate<object>(filter);
varCollectionview.View.Refresh();
}));
it seems that the View is noticed that the source is updated and deletes the sortdescriptions, but that happes delayed. All to do is add the sortdescription also delayed

ListBox doesn't show any item

I have created a ListBox with custom ListBoxItem Template, had bound everything and it worked. When I was still working on my project. I runned the program and that ListBox weren't showing items anymore. Here is my code:
This is ListBox which doesn't show any item:
<ListBox
x:Name="LB_SongList"
HorizontalAlignment="Left"
Height="498"
Margin="0,30,0,0"
VerticalAlignment="Top"
Width="319"
Background="{x:Null}"
BorderBrush="{x:Null}"
Drop="LB_SongList_Drop"
PreviewMouseMove="LB_SongList_PreviewMouseMove"
PreviewMouseDoubleClick="LB_SongList_PreviewMouseDoubleClick"
SelectionChanged="LB_SongList_SelectionChanged"
AllowDrop="True"
ScrollViewer.HorizontalScrollBarVisibility="Disabled"
ScrollViewer.VerticalScrollBarVisibility="Visible"
SelectionMode="Multiple"
ItemTemplate="{DynamicResource SongTemplate}"/>
Here is my ListBoxItem Template:
<DataTemplate x:Key="SongTemplate">
<Grid Width="302" Height="35">
<Label Content="{Binding SongName}" HorizontalAlignment="Stretch" Margin="4" VerticalAlignment="Stretch" Width="Auto" Height="Auto" Padding="2,0,0,0" VerticalContentAlignment="Center"/>
<Border BorderBrush="#B26A6A6A" BorderThickness="4" HorizontalAlignment="Stretch" Height="35" VerticalAlignment="Top" Width="Auto">
<Border.Style>
<Style>
<Style.Triggers>
<DataTrigger
Binding="{Binding Path=IsSelected, RelativeSource={RelativeSource Mode=FindAncestor, AncestorType={x:Type ListBoxItem}}}"
Value="False">
<Setter
Property="Border.Visibility"
Value="Collapsed"
/>
</DataTrigger>
</Style.Triggers>
</Style>
</Border.Style>
</Border>
</Grid>
</DataTemplate>
And the code:
public partial class MenuHolder : UserControl
{
private List<string> List_SongList;
public List<string> L_SongList
{
get { return List_SongList; }
set
{
_File.ListToObservableCollection_Song(value, O_SongList);
List_SongList = value;
}
}
public List<string> L_PlayLists
{
get
{
Settings.Default.Save();
return Settings.Default.L_PlayLists;
}
set
{
Settings.Default.L_PlayLists = value;
Settings.Default.Save();
}
}
public ObservableCollection<Song> O_SongList = new ObservableCollection<Song>();
public ObservableCollection<string> O_PlayList = new ObservableCollection<string>();
public MenuHolder()
{
InitializeComponent();
LB_SongList.ItemsSource = O_SongList;
LB_PlayList.ItemsSource = O_PlayList;
List<string> temp = new List<string>();
temp.Add("TestSong");
L_SongList = temp;
}
}
public class Song : INotifyPropertyChanged
{
public event PropertyChangedEventHandler PropertyChanged;
string _songName;
string _songLengh;
public Song(){}
public Song(String _name){ SongName = _name; }
public Song(String _name, String _lengh) { SongName = _name; SongLengh = _lengh; }
public string SongName
{
get { return _songName; }
set { _songName = value; RaisePropertyChanged("SongName"); }
}
public string SongLengh
{
get { return _songLengh; }
set { _songLengh = value; RaisePropertyChanged("SongLengh"); }
}
private void RaisePropertyChanged(string propertyName)
{
// take a copy to prevent thread issues
PropertyChangedEventHandler handler = PropertyChanged;
if (handler != null)
{
handler(this, new PropertyChangedEventArgs(propertyName));
}
}
}
And the ListToObservableCollection_Song
public void ListToObservableCollection_Song(List<string> _list, ObservableCollection<Music_Player.Menus.MainMenuObjects.Song> _collection)
{
_collection.Clear();
foreach (string _path in _list)
{
_collection.Add(new Menus.MainMenuObjects.Song(GetSongNameFromPath(_path)));
}
}
The worst thing is that it was working before and in the old version of my program it is still working. Still thinking why it is doing that.

Categories

Resources