How to programmatically create WPF scrollable StackPanel list in C# - c#

My Situation:
I'm developing a C# WPF Application (on Windows) where I need to dynamically create a lot of my controls at runtime. Because of the nature of the application, I'm not able to use standard XAML (with Templates) for many aspects of my WPF windows. This is a very unique case, and no, I'm not going to reconsider the format of my application.
What I want to accomplish:
I would like to programmatically create a control that displays a scrollable list of StackPanels (or any other effecient control group) which, for one use case, will each consist of an Image control (picture) on top of a TextBlock control (title/caption):
I would prefer to do all this without any data bindings (See below for reasoning). Because the items are being defined at runtime, I should be able to do this without them via iteration.
The control/viewer should be able to have multiple columns/rows, so it's not one dimensional (like a typical ListBox control).
It should also interchangeable so that you can modify (add, remove, etc.) the items in the control.
I've included a picture (below) to give you an example of a possible use case.
In the past, I have been able to accomplish all this by using a ListView with an ItemTemplate (wrapped in a ScrollViewer) using XAML. However, doing this entirely with C# code makes it a bit more difficult. I've recently made ControlTemplates in plain c# code (with FrameworkElementFactorys. It can get a bit complicated, and I'm not sure it's really the best practice. Should I try to go the same route (using a ListView with a template)? If so, how? Or is there a simpler, more elegant option to implement with C# code?
Edit: I would really prefer not to use any data bindings. I just want to create a (scrollable) 'list' of StackPanels that I can easily modify/tweak. Using data bindings feels like a backwards implementation and defeats the purpose of the dynamic nature of runtime.
Edit 2 (1/25/2018): Not much response. I simply need a uniform, scrollable list of stackpanels. I can tweak it to suit my needs, but it needs to be all in C# (code-behind). If anyone needs more information/clarification, please let me know. Thanks.
LINK TO XAML POST

Here's a way to do it in code using a ListBox with UniformGrid as ItemsPanelTemplate. Alternatively, you can only use a UniformGrid and put it inside a ScrollViewer, but as the ListBox already handles selection and all that stuff, you probably better stick with that one. This code will automatically adjust the number of items in a row depending on the available width.
MoviePresenter.cs :
public class MoviePresenter : ListBox
{
public MoviePresenter()
{
FrameworkElementFactory factory = new FrameworkElementFactory(typeof(UniformGrid));
factory.SetBinding(
UniformGrid.ColumnsProperty,
new Binding(nameof(ActualWidth))
{
Source = this,
Mode = BindingMode.OneWay,
Converter = new WidthToColumnsConverter()
{
ItemMinWidth = 100
}
});
ItemsPanel = new ItemsPanelTemplate()
{
VisualTree = factory
};
}
}
internal class WidthToColumnsConverter : IValueConverter
{
public double ItemMinWidth { get; set; } = 1;
public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
{
double? actualWidth = value as double?;
if (!actualWidth.HasValue)
return Binding.DoNothing;
return Math.Max(1, Math.Floor(actualWidth.Value / ItemMinWidth));
}
public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
{
throw new NotImplementedException();
}
}
MovieItem.cs :
public class MovieItem : Grid
{
public MovieItem()
{
RowDefinitions.Add(new RowDefinition() { Height = GridLength.Auto });
RowDefinitions.Add(new RowDefinition() { Height = GridLength.Auto });
RowDefinitions.Add(new RowDefinition() { Height = GridLength.Auto });
RowDefinitions.Add(new RowDefinition() { Height = GridLength.Auto });
Image image = new Image();
image.Stretch = Stretch.UniformToFill;
image.SetBinding(Image.SourceProperty, new Binding(nameof(ImageSource)) { Source = this });
Children.Add(image);
TextBlock title = new TextBlock();
title.FontSize += 1;
title.FontWeight = FontWeights.Bold;
title.Foreground = Brushes.Beige;
title.TextTrimming = TextTrimming.CharacterEllipsis;
title.SetBinding(TextBlock.TextProperty, new Binding(nameof(Title)) { Source = this });
Grid.SetRow(title, 1);
Children.Add(title);
TextBlock year = new TextBlock();
year.Foreground = Brushes.LightGray;
year.TextTrimming = TextTrimming.CharacterEllipsis;
year.SetBinding(TextBlock.TextProperty, new Binding(nameof(Year)) { Source = this });
Grid.SetRow(year, 2);
Children.Add(year);
TextBlock releaseDate = new TextBlock();
releaseDate.Foreground = Brushes.LightGray;
releaseDate.TextTrimming = TextTrimming.CharacterEllipsis;
releaseDate.SetBinding(TextBlock.TextProperty, new Binding(nameof(ReleaseDate)) { Source = this });
Grid.SetRow(releaseDate, 3);
Children.Add(releaseDate);
}
public static readonly DependencyProperty ImageSourceProperty =
DependencyProperty.Register("ImageSource", typeof(string), typeof(MovieItem), new PropertyMetadata(null));
public static readonly DependencyProperty TitleProperty =
DependencyProperty.Register("Title", typeof(string), typeof(MovieItem), new PropertyMetadata(null));
public static readonly DependencyProperty YearProperty =
DependencyProperty.Register("Year", typeof(string), typeof(MovieItem), new PropertyMetadata(null));
public static readonly DependencyProperty ReleaseDateProperty =
DependencyProperty.Register("ReleaseDate", typeof(string), typeof(MovieItem), new PropertyMetadata(null));
public string ImageSource
{
get { return (string)GetValue(ImageSourceProperty); }
set { SetValue(ImageSourceProperty, value); }
}
public string Title
{
get { return (string)GetValue(TitleProperty); }
set { SetValue(TitleProperty, value); }
}
public string Year
{
get { return (string)GetValue(YearProperty); }
set { SetValue(YearProperty, value); }
}
public string ReleaseDate
{
get { return (string)GetValue(ReleaseDateProperty); }
set { SetValue(ReleaseDateProperty, value); }
}
}
MainWindow.xaml :
<Grid>
<local:MoviePresenter x:Name="moviePresenter"
ScrollViewer.HorizontalScrollBarVisibility="Disabled"/>
</Grid>
MainWindow.xaml.cs
public partial class MainWindow : Window
{
public MainWindow()
{
InitializeComponent();
for (int i = 0; i < 20; i++)
{
DateTime dummyDate = DateTime.Now.AddMonths(-i).AddDays(-(i * i));
MovieItem item = new MovieItem()
{
ImageSource = $"http://fakeimg.pl/100x200/?text=Image_{i}",
Title = $"Dummy movie {i}",
Year = $"{dummyDate.Year}",
ReleaseDate = $"{dummyDate.ToLongDateString()}"
};
moviePresenter.Items.Add(item);
}
}
}

Related

How to recycle / reuse list of children views without having to make it recalculate size for better performance? Xamarin Forms

I am currently working with a Stacklayout that has list support. I load the data for this list on another thread, then add it to this view on the main thread. This works fine functionality wise, but I am having a slight delay / performance issue when the items are being loaded to the control. So after the call to the API has been made, and the assignment to the main thread is happening. I load around 5 items to ensure that i do not crowd it since it contains images (that are small in size + i use FFImageLoading).
What i have done now is to cache all of the views and then reuse it when possible. Works fine, but does not seem to improve the performance a lot. What im thinking of is to override LayoutChildren because it appears that the children that is added to this stacklayout, is recalculating it's size even though I reuse the View. What might i be doing wrong?
Below is my custom control:
public class CustomStackLayout : StackLayout
{
public static readonly BindableProperty ItemTemplateProperty = BindableProperty.Create(nameof(ItemTemplate), typeof(DataTemplate), typeof(CustomStackLayout),
default(DataTemplate), BindingMode.OneWay);
public DataTemplate ItemTemplate
{
get { return (DataTemplate)GetValue(ItemTemplateProperty); }
set
{
SetValue(ItemTemplateProperty, value);
OnPropertyChanged(nameof(ItemTemplate));
}
}
public static readonly BindableProperty ItemsSourceProperty =
BindableProperty.Create("ItemsSource", typeof(IEnumerable), typeof(CustomStackLayout), default(IEnumerable<object>), BindingMode.TwoWay, propertyChanged: ItemsSourceChanged);
public IEnumerable ItemsSource
{
get { return (IEnumerable)GetValue(ItemsSourceProperty); }
set
{
SetValue(ItemsSourceProperty, value);
OnPropertyChanged(nameof(ItemsSource));
}
}
private static void ItemsSourceChanged(BindableObject bindable, object oldValue, object newValue)
{
var items = (CustomStackLayout)bindable;
items.Children.Clear();
if (items.ItemsSource != null)
{
foreach (var item in items.ItemsSource)
{
items.Children.Add(items.GetRowView(item));
}
}
}
protected virtual View GetRowView(object item)
{
// See if cached
var model = item as CustomModel;
if (AllViews.Any(x => x.Message.ObjectID.Equals(model.ObjectID)))
{
return AllViews.Where(x => x.Message.ObjectID.Equals(model.ObjectID)).FirstOrDefault().View;
}
object viewContent;
// Determine if this is a straight up template or using a AllViewselector
if (ItemTemplate is DataTemplateSelector dts)
{
var template = dts.SelectTemplate(item, this);
viewContent = template.CreateContent();
}
else
{
viewContent = ItemTemplate.CreateContent();
}
// Cache views
this.AllViews.Add(new CachedView() { Message = msg, View = viewContent is View ? viewContent as View : ((ViewCell)viewContent).View });
// Bind
var rowView = viewContent is View ? viewContent as View : ((ViewCell)viewContent).View;
rowView.BindingContext = item;
return rowView;
}
public List<CachedView> AllViews = new List<CachedView>();
}
public class CachedView
{
public Type Type { get; set; }
public View View { get; set; }
public Message Message { get; set; }
}
This is how i use it in xaml with a DataTemplateSelector that picks between different views depending on the data:
<controls:CustomStackLayout ItemsSource="{Binding Data}">
<controls:CustomStackLayout.ItemTemplate>
<selector:MyDataTemplateSelector />
</controls:CustomStackLayout.ItemTemplate>
</controls:CustomStackLayout>

Properties of custom control can only be filled by StaticResource but not with values defined in XAML

I've created my own ExpandableView based on this https://www.clearpeople.com/insights/blog/2019/February/how-to-create-a-contentview-with-expandable-functionality
but as all C# code.
My control looks like this (without the animation part)
public class ExpandableView : ContentView
{
public static readonly BindableProperty ExpandableContentProperty = BindableProperty.Create(nameof(ExpandableContent), typeof(View), typeof(ExpandableView));
public static readonly BindableProperty TitleTextProperty = BindableProperty.Create(nameof(TitleText), typeof(string), typeof(ExpandableView));
public View ExpandableContent
{
get => this._content;
set
{
if (this._content == value)
{
return;
}
OnPropertyChanging();
if (this._content != null)
{
this._ContentLayout.Children.Remove(this._content);
}
this._content = value;
this._ContentLayout.Children.Add(this._content);
OnPropertyChanged();
}
}
public string TitleText
{
get => this._Title.Text;
set
{
if (this._Title.Text == value)
{
return;
}
OnPropertyChanging();
this._Title.Text = value;
OnPropertyChanged();
}
}
private readonly StackLayout _OuterLayout;
private readonly StackLayout _ContentLayout;
private readonly StackLayout _TitleLayout;
private View _content;
private readonly Label _Title;
public ExpandableView()
{
this._OuterLayout = new StackLayout();
this._ContentLayout = new StackLayout();
this._TitleLayout = new StackLayout
{
Orientation = StackOrientation.Horizontal,
};
this._Title = new Label
{
HorizontalOptions = new LayoutOptions(LayoutAlignment.Start, true),
HorizontalTextAlignment = TextAlignment.Center,
VerticalTextAlignment = TextAlignment.Center,
Text = "Title",
};
this._Title.FontSize = Device.GetNamedSize(NamedSize.Medium, this._Title);
this._TitleLayout.Children.Add(this._Title);
this._OuterLayout.Children.Add(this._TitleLayout);
this._OuterLayout.Children.Add(this._ContentLayout);
Content = this._OuterLayout;
}
}
But now, when I try to use it in XAML as I normally would:
<controls:ExpandableView TitleText="Equipment">
<controls:ExpandableView.ExpandableContent>
<StackLayout>
<Label Text="EQ_12345" />
<Button Command="{Binding ShowDatacommand}" />
</StackLayout>
</controls:ExpandableView.ExpandableContent>
</controls:ExpandableView>
Setting the properties to some values results in the title still showing me "Title" and no content being shown. If I instead put everything into the StaticResource Everything works just fine:
<controls:ExpandableView ExpandableContent="{StaticResource ExpendableViewContent}"
TitleText="{StaticResource EquiString}" />
While testing, I set some breakpoints within the properties, and only when I used {StaticResource} the properties were set. All values defined directly in XAML were never passed to the properties. What am I doing wrong here?
When defining your own BindableProperty properties, the definitive source for the values is expected to be accessed via BindableObject.SetValue/BindableObject.GetValue. The Xamarin runtime can directly use that rather than going through your get/set methods.
Using TitleText as an example, the implementation should be something like:
public string TitleText
{
get => (string)GetValue(TitleTextProperty);
set
{
SetValue(TitleTextProperty, value);
}
}
The linked article does do this.
In order to create the link between the property and the displayed title, establish data binding in the constructor to link the Text of the title label to the TitleText property in the ExpandableView constructor:
_Title.SetBinding(Label.TextProperty, new Binding(nameof(TitleText)) { Source = this });

Bindable property return only default value

I want create user control (user view) in xamarin.forms. My contol has one property. Control choose what the element would be add to the page (Entry or Label). For it I use BindableProperty, but it return only default value. I dont understand what wrong?
Here my user control code:
public partial class UserView : ContentView
{
public UserView()
{
InitializeComponent();
StackLayout stackLayout = new StackLayout();
if (TypeElement == "label") //TypeElement return only "default value"
stackLayout.Children.Add(new Label { Text = "LABEL" });
else
stackLayout.Children.Add(new Entry { Text = "ENTRY" });
Content = stackLayout;
}
public static readonly BindableProperty TypeProperty = BindableProperty.CreateAttached("TypeElement", typeof(string), typeof(UserView), "default value");
public string TypeElement
{
get
{
return (string)GetValue(TypeProperty);
}
set
{
SetValue(TypeProperty, value);
}
}
}
Here I use my control on page:
Your TypeElement property is getting set after the constructor completes, you should be watching for when this property changes and then do what you need to do, for example:
public static readonly BindableProperty TypeProperty = BindableProperty.CreateAttached("TypeElement", typeof(string), typeof(UserView), "default value", propertyChanged:OnTypeElementChanged);
private static void OnTypeElementChanged(BindableObject bindable, object oldValue, object newValue)
{
var userView = bindable as UserView;
StackLayout stackLayout = new StackLayout();
if (userView.TypeElement == "label") //TypeElement return only "default value"
stackLayout.Children.Add(new Label { Text = "LABEL" });
else
stackLayout.Children.Add(new Entry { Text = "ENTRY" });
userView.Content = stackLayout;
}
I have tested this and it works, there are a couple of things about your implementation that confuse me though, such as why you are using an attached property instead of a regular bindable property, and also why you seem to have a XAML file associated with UserView if you're just going to replace the content anyway.

Xamarin Forms Custom ViewCell, don't add to grid if empty?

I've created a Custom Viewcell, where i bind text to a label and then insert it in a grid. However, how do I avoid empty rows, if the text I pass to the Viewcell is empty? This is just some of the code, but is there somekind of binding I am missing if the text is empty?
public RouteElementsCustomCell()
{
Label NameLbl = new Label()
{
TextColor = Color.Black,
HorizontalTextAlignment = TextAlignment.Center,
FontSize = Device.GetNamedSize(NamedSize.Large, typeof(Label))
};
NameLbl.SetBinding(Label.TextProperty, "StopName");
Grid grid = new Grid()
{
Padding = 10,
RowDefinitions =
{
new RowDefinition
{
Height = GridLength.Auto
},
}
};
grid.Children.Add(NameLbl,0,1,0,1);
}
Alright, so this is how I solved this issue with help from #irreal.
This is probably introducing unneeded complexity to your viewmodel. Consider using a xaml value converter which would convert string to boolean. You would then just bind IsVisible="{Binding StopName, Converter={}}" It is super useful, and would allow you to do lots of things, including control visibility based on string not being null or empty - #irreal
Added the Label.IsVisibleProperty to the label, then used an IValueConverter, to check if the string was empty, null or whitespace.
Label
Label NameLbl = new Label()
{
TextColor = Color.Black,
HorizontalTextAlignment = TextAlignment.Center,
VerticalOptions = LayoutOptions.Start,
FontSize = Device.GetNamedSize(NamedSize.Large, typeof(Label))
};
NameLbl.SetBinding(Label.TextProperty, "StopName");
NameLbl.SetBinding(Label.IsVisibleProperty, "StopName",BindingMode.Default,new StringToBoolConverter(), null);
ValueConverter
public class StringToBoolConverter : IValueConverter
{
public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
{
string valueAsString = value.ToString();
if (string.IsNullOrWhiteSpace(valueAsString))
{
return false;
}
else
{
return true;
}
}
public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
{
return null;
}
}
What I would do is add a public property to my object:
public bool ShowItem { get{return !string.IsNullOrEmpty(StopName)};}
Then bind the IsVisibleProperty to ShowItem

Get Dependency Property Value in UserControl Code Behind

I'm new to WPF. I'm facing the following issue. I have a User Control, Here is the Code of User Control
CustomControl Code
public partial class CustomCanvas : UserControl
{
public CustomCanvas()
{
InitializeComponent();
DrawCanvas();
}
private void DrawCanvas()
{
//TODO:
//Get the Dictionary Value from Parent Bound Property
}
public Dictionary<string, List<Shapes>> ShapesData
{
get { return (Dictionary<string, List<Shapes>>)GetValue(ShapesDataProperty); }
set { SetValue(ShapesDataProperty, value); }
}
public static readonly DependencyProperty ShapesDataProperty =
DependencyProperty.Register("ShapesData", typeof(Dictionary<string, List<Shapes>>), typeof(CustomCanvas), new PropertyMetadata(ShapesDataChanged));
private static void ShapesDataChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
var value = e.NewValue;
}
}
Main Window Code
<Grid>
<uc:CustomCanvas ShapesData="{Binding ShData}" ></uc:CustomCanvas>
</Grid>
Value of ShapesData is bounded to following code.
ShData = new Dictionary<string, List<Shapes>>()
{
{"Week 1", new List<Shapes>() {Shapes.Circle, Shapes.Rectangle}},
{"Week 2", new List<Shapes>() {Shapes.Circle}},
{"Week 3", new List<Shapes>() {Shapes.Rectangle}}
};
Now My question is that in CustomControl DrawCanvas Method i want to fetch the bounded value in parent. Could any one guide me regarding that.
P.S: I know how to bound this value in child using relative RelativeSource and Mode as FindAncestor. Here i want to just fetch the value and process that Dictionary data. In ShapesDataChanged i can easily access the data but the issue is in fetching it in DrawCanvas Function.
Thanks in advance.
You can use DependencyObject's GetValue() method.
var theValueYouNeeded = CustomCanvas.GetValue(ShapesDataProperty);
Dictionary<string, List<Shapes>> value = (Dictionary<string, List<Shapes>>)theValueYouNeeded;
....

Categories