Limit GridSplitter (MVVM) - c#

Simple case, when I want to limit GridSplitter maximum, then I can create invisible control (which only participates in layouting), adjust its Margin and use to limit grid column/row definitions:
<Grid x:Name="limiter" Margin="10,10,20,10" />
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="{Binding StoreWidth, Converter={local:DoubleToGridLengthConverter}, Mode=TwoWay}"
MaxWidth="{Binding ActualWidth, ElementName=gridLimit}" /> <!-- limit maximum -->
...
</Grid.ColumnDefinitions>
<GridSplitter ... />
...
</Grid>
Now complicated case: nested view with GridSplitter, which has to be limited by some layout logic of parent view.
How do I do this? I am seeking for a comfortable and reusable solution.
Currently I am doing it complicated way:
add property to parent VM to bind limiter values (e.g. ActualWidth of it);
add to nested VM property Parent;
pass parent VM instance to nested one (using property initializer);
now GridSplitter can be limited by using Parent.SplitterMaxWidth binding.
Is there any nice and MVVM-friendly approach to the problem?

For cases when GridSplitter has to be limited by parent (TBH all my layouts can be converted to ensure this) it is possible to bind to that parent and use generic converter to change value. If parent is hard to reach, then make a simple custom control:
public class GridLimiter: Grid { }
which can be used to mark that container to be easily reached by RelativeSource.FindAncestor:
<local:GridLimiter ... >
... <!-- deeply nested -->
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition MaxWidth="{Binding ActualWidth, RelativeSource={RelativeSource FindAncestor, AncestorType={x:Type local:GridLimiter}}}" />
...
</Grid.ColumnDefinitions>
<GridSplitter ... />
</Grid>
...
Moreover, making a simple markup extension converter:
public class ValueAddConverter : MarkupExtension, IValueConverter
{
public ValueAddConverter() { }
public override object ProvideValue(IServiceProvider serviceProvider) => this;
public object Convert(object value, Type targetType, object parameter, CultureInfo culture) =>
(double)value + double.Parse((string)parameter, NumberFormatInfo.InvariantInfo);
public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
{
throw new NotImplementedException();
}
}
allows to define offset without needs to create invisible limiter:
MaxWidth="{Binding ActualWidth, Converter={local:ValueAddConverter}, ConverterParameter=-30, RelativeSource={RelativeSource FindAncestor, AncestorType={x:Type local:GridLimiter}}}"

Related

WPF Xaml Grid - can I number columns starting from the end (e.g. Grid.Column="-1")

I have a WPF grid with many rows and columns, written with Xaml.
In the grid, I want to place an element in the last (rightmost) column.
The problem is, if I need to add another column, the all items on the right move left by one.
Is there a way to specify in (preferred Xaml only) the index from the end, similar to indexing in some languages mylist[-1] to denote the last item.
If you set Grid.Column to a number higher than the number of columns, it will put the element in the last column. So you could set Grid.Column to something like Int32.MaxValue (or any number that you can guarantee will always be greater than the number of columns) and that element will always be in the last column no matter how many new columns you add.
You can't do it in pure XAML, but you can do it by binding the Grid.Column property to the number of grid columns and using a converter to add the offset from the end:
public class OffsetConverter : IValueConverter
{
public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
{
return (int)value + Int32.Parse(parameter?.ToString());
}
public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
{
return Binding.DoNothing;
}
}
Which you then use like this:
<Window.Resources>
<local:OffsetConverter x:Key="OffsetConverter" />
</Window.Resources>
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition />
<ColumnDefinition />
<ColumnDefinition />
<ColumnDefinition />
</Grid.ColumnDefinitions>
<TextBlock Text="Col 0" Grid.Column="0" />
<TextBlock Text="Col 1" Grid.Column="1" />
<TextBlock Text="Col -2" Grid.Column="{Binding Path=ColumnDefinitions.Count, RelativeSource={RelativeSource AncestorType=Grid}, Converter={StaticResource OffsetConverter}, ConverterParameter=-2}" />
<TextBlock Text="Col -1" Grid.Column="{Binding Path=ColumnDefinitions.Count, RelativeSource={RelativeSource AncestorType=Grid}, Converter={StaticResource OffsetConverter}, ConverterParameter=-1}" />
</Grid>
Result:
Only problem with this specific implementation is that it uses CommandParameter to pass in the offset, which means it has to be a constant hard-coded into the XAML. If you need to bind it to another property then use a MultiConverter instead and pass it in as the second parameter.
While this does work, the fact you need to do it suggests you may have a more fundamental, underlying architectural problem. Even if you're not doing full-blown MVVM, the data you bind your front-end to should be readying that data
for easy consumption by the view. In this case, that layer should probably be aware of the number of columns itself and also be able to calculate the column number of each element that you're displaying. The fact that you're being forced to calculate all this after-the-fact, and on the other side of the view, suggests you might want to re-think your architecture (assuming, of course, you're in full control of it and also have time to do it).

Why is this WPF custom DependencyProperty binding on value converter not executing?

I am building what I have learned is basically an accordion control, with a selection mode that ensures that only one section is open at the time. Each section is implemented using Expander controls, so if on Expander is opened, all others should close.
I have done that in the following manner:
Added a property representing the Id of the currently open section, ActiveQuestionId on the view model (which implements INotifyPropertyChanged)
Created a converter inheriting from DependencyObject that is able to convert the ActiveQuestionId to a boolean indicating whether a specific section should be open, by adding a DependencyProperty ControlValue to the converter that indicates which section that it belongs to
Creating a local converter for each section with ControlValue bound to the QuestionId of its section
While the converter methods executes successfully, the problem is the DependencyProperty ControlValue is never set even though it binds successfully to a value and don't raise any errors. I have confirmed this through various debugging. So the result is that all sections are stuck with the default value, rendering the accordion selection behavior I want, useless.
Why is the DependencyProperty binding being ignored? Is it because it is defined within a binding, or something else?
Remarks
Everything is data-driven, and worked great in static a mockup I did before implementing the generic data-driven version. A fully data driven solution is a must, so using one way multi bindings or hardcoded XAML parameters (the solutions I have been able to find for related issues) is not an option.
It is important to note that all other bindings work perfect, so there is no problem DataContext wise. As everything should work (in my mind), this is also why I have not gone the WPF Toolkit Accordion way yet, so please do not suggest this initially (unless it is really the only way). First of, being new to WPF, I would like to understand why this is not working.
XAML (extract - some names changed to obfuscate business meaning - central part is IsExpanded binding):
<ItemsControl ItemsSource="{Binding QuestionSection.QuestionAssignments}"
VerticalAlignment="Stretch"
HorizontalAlignment="Stretch"
Style="{x:Null}">
<ItemsControl.ItemTemplate>
<DataTemplate>
<Border BorderBrush="{StaticResource Grey400Brush}"
BorderThickness="0 1 0 0">
<Expander Background="{StaticResource Grey200Brush}"
Foreground="Black"
Padding="0"
Margin="0">
<Expander.IsExpanded>
<Binding RelativeSource="{RelativeSource FindAncestor, AncestorType=views:TypeOfParentControl}"
Path="DataContext.ActiveQuestionId"
Mode="TwoWay">
<Binding.Converter>
<converters:TestConverter ControlValue="{Binding QuestionId}"/>
</Binding.Converter>
</Binding>
</Expander.IsExpanded>
<Expander.HeaderTemplate>
<!--Custom Styling Here, All Bindings Work-->
</Expander.HeaderTemplate>
<!--Content Here, All Bindings Work-->
</Expander>
</Border>
</DataTemplate>
</ItemsControl.ItemTemplate>
Converter (simplified)
public class TestConverter : DependencyObject, IValueConverter
{
public static readonly DependencyProperty ControlValueProperty = DependencyProperty.Register("ControlValue", typeof(short), typeof(TestConverter), new PropertyMetadata(default(short)));
public short ControlValue
{
get { return (short) GetValue(ControlValueProperty); }
set { SetValue(ControlValueProperty, value); }
}
public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
{
return (short)value==ControlValue;
}
public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
{
return (bool)value ? ControlValue : Binding.DoNothing;
}
}
ActiveQuestionId implementation in ViewModel - INotifyPropertyChanged is tested and works, ViewModel is DataContext on Parent UserControl
private short activeQuestionId;
public short ActiveQuestionId
{
get
{
return activeQuestionId;
}
set
{
if (value != activeQuestionId)
{
activeQuestionId = value;
OnPropertyChanged();
}
}
}
The current DataContext value is not inherited down to the TestConverter instance.
You may avoid this complex binding altogether and implement your control by using a ListBox:
<ListBox ItemsSource="{Binding QuestionSection.QuestionAssignments}"
SelectedValuePath="QuestionId"
SelectedValue="{Binding ActiveQuestionId}">
<ListBox.ItemContainerStyle>
<Style TargetType="ListBoxItem">
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="ListBoxItem">
<Expander IsExpanded="{Binding IsSelected,
RelativeSource={RelativeSource TemplatedParent}}">
...
</Expander>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
</ListBox.ItemContainerStyle>
</ListBox>

Is there a way to to Indent certain items in a list view using c# for a windows 8 store app?

Basically, I have a list of items and sub items (all in one list). I want to indent certain specific items that are actually sub items. Is there a function or property that I can use to do this? I've tried googling for it, and even searching for it here on stack overflow - but no success.
NOTE: I'm using C# and XAML for a windows 8 store application. Not a wpf app (the documentation and code differ sometimes).
EDIT:
Here's what my XAML looks like:
<ListView
x:Name="ListView"
AutomationProperties.AutomationId="ListView"
AutomationProperties.Name="items"
TabIndex="1"
Grid.Row="1"
Margin="-10,-10,0,0"
Padding="20,0,0,45"
IsSwipeEnabled="False"
SelectionChanged="ListView_SelectionChanged">
<ListView.ItemTemplate>
<DataTemplate>
<Grid Margin="1">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto"/>
<ColumnDefinition Width="*"/>
</Grid.ColumnDefinitions>
<StackPanel Grid.Column="0" Margin="10,0,0,0">
<TextBlock x:Name="Item" Tag="{Binding ID}" Text="{Binding Name}" Style="{StaticResource TitleTextBlockStyle}" TextWrapping="Wrap" MaxHeight="40" FontSize="14" FontFamily="Global User Interface"/>
</StackPanel>
</Grid>
</DataTemplate>
</ListView.ItemTemplate>
</ListView>
The listview is binded to an observablelist of objects.
To Expand further on another answer here... The suggested route was to add a property to the underlying class (in this case, MyClass) and bind the ItemsSource for the ListView to a list of these objects.
public class MyClass
{
public bool IsSubItem { get; set; }
}
// Elsewhere in your code, you would need a list of these object
public ObservableCollection<MyClass> MyList { get; set; }
You would also need a Converter class, which is very easy to setup:
You'd need a converter class; converters are really easy once you get the hang of them. A simple example for this scenario would be:
public class BoolToMarginConverter : IValueConverter
{
public object Convert(object value, Type targetType, object parameter, string language)
{
//Get the passed in value and convert it to a boolean - the as keyword returns a null if we can't convert to a boolean so the ?? allows us to set a default value if we get a null instead
bool isSubItem = (value as bool?) ?? false;
// If the item IS a sub item, we want a larger Left Margin
// Since the Margin Property expects a Thickness, that's what we need to return
return new Thickness(isSubItem == true ? 24 : 12, 0, 0, 0);
}
// This isn't necessary in most cases, and in this case can be ignored (just required for the Interface definition)
public object ConvertBack(object value, Type targetType, object parameter, string language)
{
throw new NotImplementedException();
}
}
Once you get this setup, you'll need to add this to your XAML as a resource:
// Add a reference to your namespace if it isn't already in your XAML
xmlns:local="using:MyNamespace"
// You'll also need to add a static resource
<Page.Resources>
<local:BoolToMarginConverter x:Key="BoolToMarginConverter" />
</Page.Resources>
// And then lastly, you'll need to update your ListView XAML to use the new information
<ListView
x:Name="ListView"
ItemsSource={Binding MyList}
<!-- Other Properties removed for space -->
<ListView.ItemTemplate>
<DataTemplate>
<Grid Margin="1">
<!-- Other info removed for space -->
<StackPanel Grid.Column="0" Margin="{Binding Path=IsSubItem, Converter={StaticResource BoolToMarginConverter}}">
<!-- Other info removed for space -->
</StackPanel>
</Grid>
</DataTemplate>
</ListView.ItemTemplate>
</ListView>
If you just want to indent the text of the sub-items you could change the margin of the text block or stack panel.
To do this you would do the following:
Create a class for the items you will be adding to the list view
Add to the class, an IsSubItem property
Create an observable collection of these items and bind them to your list view source.
In the ListView template, bind the stack panel or text block margin to the IsSubItem property using a converter to convert the IsSubItem boolean into the appropriate margin.

WP8 - Binding a value with a float to String conversion

I have a couple of gauges in my app, and I can't figure out how to add a converter to the text binding. I read a couple of guides on msdn but I didn't manage to figure that out (I've been coding for WP8 for just a couple of weeks).
This is is a piece of the gauge:
<gauges:MarkerGaugeIndicator Value="0"
gauges:LinearGaugeRange.IndicatorOffset="35"
x:Name="GaugeBarValore"
IsAnimated="True">
<gauges:MarkerGaugeIndicator.MarkerTemplate>
<DataTemplate>
<Grid Width="73" Height="35" UseLayoutRounding="False" d:LayoutRounding="Auto" Margin="10,-2,0,0">
<TextBlock x:Name="GaugeBarPercent" Text="{Binding}"
HorizontalAlignment="Center"
VerticalAlignment="Center"
FontSize="20"
FontWeight="Thin" Margin="6,4,32,4" Width="35"/>
<Grid.RenderTransform>
<CompositeTransform Rotation="90" TranslateX="49" TranslateY="12" />
</Grid.RenderTransform>
</Grid>
</DataTemplate>
</gauges:MarkerGaugeIndicator.MarkerTemplate>
</gauges:MarkerGaugeIndicator>
The binding itself works, but I can see a lot of decimal numbers while the value moves from a round value to another.
I want to add a converter like this method:
private String double2String(double valore)
{
return Convert.ToString(Math.Round(valore)) + "%";
}
I just don't know where to put this method and how to add this as a converter inside the binding.
Thank you for your help! :)
Sergio
Create a class to hold your Converter method that implements IValueConverter interface, Example class is bellow. You have to implement method Convert and ConvertBack.
public class DoubleToString : IValueConverter
{
public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
{
return Math.Round((double)value).ToString() + "%";
}
public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
{
return double.Parse(value as string);
}
}
then add the namespace to your XAML page.
xmlns:convert="clr-namespace:Your_project_name"
Next add your converter as a Resource type i to your XAML page..
<phone:PhoneApplicationPage.Resources>
<convert:DoubleToString x:Key="DoubleConvert" />
</phone:PhoneApplicationPage.Resources>
The x:Key value is the name we are going to call in our binding statement.
Then perform the data binding. I have a simple slider and a textblock with sliders value bound to the textblocks Text property
<StackPanel>
<Slider Name="slider" Maximum="100" Minimum="0" />
<TextBlock Text="{Binding Value, ElementName=slider, Converter={StaticResource DoubleConvert}}" />
</StackPanel>
Define this converter as a resource in your parent view
<UserControl.Resources>
<local:double2String x:Key="convertDouble" />
</UserControl.Resources>
And add it to the binding
<TextBlock x:Name="GaugeBarPercent" Text="{Binding, Converter={StaticResource convertDouble}}"
Don't forget to import the namespace where the converter is defined to your view
xmlns:local="clr-namespace:YOUR_NAMESPACE"
The easier way is to use StringFormat. Like this:
<Label Text="{Binding Path=SomeProperty, StringFormat='{0:F2}%' }"/>

Applying transforms from DataTemplates in WPF

I've created a DataTemplate for my objects in a ResourceDictionary file. The template is basically an image that is loaded from the disk. Now, what happens is that I want to align the image to a specific point on my Canvas but not by its upper left point but its center point, that's why I want to apply a translate transform for X = -Width / 2 and
Y = -Height / 2 but I don't know how to apply them via the DataTemplate.
Any help will be appreciated, thanks!
Try using databinding on Canvas' the AttachedProperties and an IValueConverter to transform the offsets to whatever you want.
For instance:
class ImageToCanvasConverter : IValueConverter
{
public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
{
return -(int)value / 2;
}
public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
{
// Two-way binding not supported
throw new InvalidOperationException();
}
}
<Grid.Resources>
<myAssembly:ImageToCanvasConverter x:Key="imageToCanvasConverter" />
<DataTemplate ...>
<Image Canvas.Left="{Binding Path=Width, Converter={StaticResource imageToCanvasConverter}, Mode=OneTime}"
Canvas.Top="{Binding Path=Height, Converter={StaticResource imageToCanvasConverter}, Mode=OneTime}"
... />
</DataTemplate>
</Grid.Resources>
you can take the advantage of using loaded event on the data template child
Example:
if you are using grid as data template content
<DataTemplate>
<Grid Loaded="Grid_Loaded">
<Image></Image>
</Grid>
</DataTemplate>
you can write the transformation code in the .cs file using sender object.

Categories

Resources