Silverlight - What does my datacontext/binding path need to be here? - wpf

I've been working a bit with an image editor in Silverlight for the past week or so. It's my first encounter with it and I still haven't fully gotten my head around data bindings and datacontext or mvvm. I have a rotation method and I want to be able to pass the angle value from a text box on my MainPage.xaml to the method.I have an initial value set of 90 and my function rotates the image 90 degrees when I click it. The textbox is empty at runtime and also is clearly not updating my rotation angle.
MainPage.xaml
<Grid DataContext="{Binding Path=Project}" Height="70" Name="grid1" Width="200">
<Grid.RowDefinitions>
<RowDefinition Height="35" />
<RowDefinition Height="35" />
</Grid.RowDefinitions>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="84*" />
<ColumnDefinition Width="57*" />
<ColumnDefinition Width="59*" />
</Grid.ColumnDefinitions>
<Button Command="{Binding Path=RotateCWElementCommand}"
Height="30" Name="btnRotCW" Width="30" Grid.Column="2" Margin="15,0,14,5" Grid.Row="1">
<Image Source="../Assets/Images/Icons/object-rotate-right.png" Grid.Column="1"
Grid.Row="4" Height="20" HorizontalAlignment="Left" Name="image1" Stretch="Fill" VerticalAlignment="Top" Width="20" />
</Button>
<TextBlock FontWeight="Bold" HorizontalAlignment="Left" Margin="10,10,0,8" Text="Rotate" VerticalAlignment="Center" />
<TextBlock FontWeight="Bold" HorizontalAlignment="Left" Margin="34,9,0,10" Text="Angle:" VerticalAlignment="Center" Grid.Row="1" />
<TextBox Text="{Binding Path=RotateElement.Angle, Mode=TwoWay}" Height="24" Name="textBox1" Width="52" Grid.Column="1" Margin="0,6,5,5" Grid.Row="1" />
</Grid>
(Relevant code from)
Project.cs-
namespace ImageEditor.Client.BLL
{
public class Project : INotifyPropertyChanged
{
#region Properties
private RotateTransform rotateElement;
public RotateTransform RotateElement
{
get { return rotateElement; }
set
{
rotateElement = value;
NotifyPropertyChanged("RotateElement");
}
}
#endregion
#region Methods
private void RotateCWElement(object param)
{
FrameworkElement element = this.SelectedElement;
RotateTransform RotateElement = new RotateTransform();
RotateElement.Angle = 90;
RotateElement.CenterX = element.ActualWidth * 0.5;
RotateElement.CenterY = element.ActualHeight * 0.5;
element.RenderTransform = RotateElement;
}
What am I doing wrong here? Is it my datacontext or my binding path that is the problem? What should they be? Formatting is a little off sorry

the UI does not know that a property in your object has changed, since you only notify when your object changes, but not the properties in it:
private void RotateCWElement(object param)
{
FrameworkElement element = this.SelectedElement;
if (this.RotateElement == null) this.RotateElement = new RotateTransform();
RotateElement.Angle = 90;
RotateElement.CenterX = element.ActualWidth * 0.5;
RotateElement.CenterY = element.ActualHeight * 0.5;
element.RenderTransform = RotateElement;
//tell the UI that this property has changed
NotifyPropertyChanged("RotateElement");
}

Related

Creation of controls dynamically on a separate thread

I am developing a WPF app, to generate folders dynamically
Right now I have an issue with what happens if the user puts 10000 folders? I am trying to run my for loop in another thread
By the way, how can I limit the user to create x number of folders?
Code Behind:
public MainWindowVM() {
SaveCommand = new Command(SaveAction);
FoldersCollection = new ObservableCollection<FolderData>();
CreateCommand = new Command(CreateAction);
CreateFoldersCommand = new Command(CreateFolderAction);
}
private void SaveAction() {
}
private void CreateFolderAction() {
var dirrName = "Assets";
var workingDirectory = Environment.CurrentDirectory;
// This will get the current PROJECT directory
var projectDirectories = Directory.GetParent(workingDirectory).Parent.Parent.GetDirectories(dirrName)
.ToList();
var path = Path.GetFullPath(projectDirectories.First().FullName);
foreach (var item in FoldersCollection) {
Directory.CreateDirectory($"{path}\\{item.FolderName}");
}
}
private void CreateAction() {
for (int i = 0; i < Value; i++) {
Application.Current.Dispatcher.Invoke(DispatcherPriority.Background, new ThreadStart(delegate {
FoldersCollection.Add(new FolderData() { FolderID = i, FolderName = string.Empty, Extenion = new List<string>() });
}));
XAML:
<WrapPanel>
<Grid Margin="10">
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
<RowDefinition Height="600" />
<RowDefinition Height="Auto" />
</Grid.RowDefinitions>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="110" />
<ColumnDefinition Width="400" />
</Grid.ColumnDefinitions>
<Label Content="Number of folders: " />
<TextBox
Grid.Column="1"
VerticalAlignment="Center"
PreviewTextInput="FolderNames_count"
Text="{Binding Value, UpdateSourceTrigger=PropertyChanged}" />
<Button
Grid.Column="1"
HorizontalAlignment="Right"
VerticalAlignment="Center"
BorderBrush="Transparent"
Command="{Binding CreateCommand}"
Content="Create"
Focusable="False" />
<ScrollViewer
Grid.Row="1"
Grid.ColumnSpan="2"
Margin="10"
BorderBrush="Transparent"
Focusable="False"
HorizontalScrollBarVisibility="Auto">
<ItemsControl ItemsSource="{Binding FoldersCollection}">
<ItemsControl.ItemTemplate>
<DataTemplate>
<StackPanel Orientation="Horizontal">
<TextBlock
Margin="0,10,0,0"
VerticalAlignment="Center"
FontStyle="Oblique"
Text="{Binding FolderID}" />
<syncfusion:SfTextBoxExt
Width="300"
Margin="10,10,0,0"
VerticalAlignment="Center"
Text="{Binding FolderName, UpdateSourceTrigger=PropertyChanged}"
Watermark="Folder Name" />
</StackPanel>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
</ScrollViewer>
<Button
Grid.Row="2"
VerticalAlignment="Center"
Command="{Binding CreateFoldersCommand}"
Content="Create structure"
Style="{StaticResource MenuItem}" />
<Button
Grid.Row="2"
Grid.Column="1"
Margin="5,0,0,0"
VerticalAlignment="Center"
Command="{Binding SaveCommand}"
Content="Save structure"
Style="{StaticResource MenuItem}" />
</Grid>
</WrapPanel>
The app works fine, but when I exit, I get this error
btw I am doing this to allow numbers in my textbox
private void FolderNames_count(object sender, TextCompositionEventArgs e) {
if (!char.IsDigit(e.Text, e.Text.Length - 1))
e.Handled = true;
}
If you commands run on separate thread, then it looks like the bg thread still running during GUI thread is finished.
My recommendation is to put the handling of this case to the Window.Closing handler, i.e. do notify thread to break the loop, e.g. set cancellation token.
The brutal option would be to call Enviroment.Exit(0) method in the Window.Closing or Window.Closed handler, to terminate all threads. See Environment.Exit(Int32) Method
It's the loop should be executed on a background thread:
private void CreateAction()
{
Task.Run(() =>
{
for (int i = 0; i < Value; i++)
{
Application.Current.Dispatcher.BeginInvoke(DispatcherPriority.Background, new Action(() =>
FoldersCollection.Add(new FolderData() { FolderID = i })));
}
});
}

ListView ItemsPanelTemplate with horizontal orientation, how to Identify ListViewItems on first row?

First of all I am working with MVVM / WPF / .Net Framework 4.6.1
I have a ListView configured with ItemsPanelTemplate in horizontal orientation that displays items from a DataTemplate. This setup allows me to fit as many items inside the Width of the ListView (the witdth size is the same from the Window), and behaves responsively when I resize the window.
So far everything is fine, now I just want to Identify what items are positioned on the first row, including when the window get resized and items inside the first row increase or decrease.
I merely want to accomplish this behavior because I would like to apply a different template style for those items (let's say a I bigger image or different text color).
Here below the XAML definition for the ListView:
<ListView x:Name="lv"
ItemsSource="{Binding Path = ItemsSource}"
SelectedItem="{Binding Path = SelectedItem}">
<ListView.ItemsPanel>
<ItemsPanelTemplate>
<WrapPanel Orientation="Horizontal"></WrapPanel>
</ItemsPanelTemplate>
</ListView.ItemsPanel>
<ListView.ItemTemplate>
<DataTemplate>
<Grid Width="180" Height="35">
<Grid.RowDefinitions>
<RowDefinition />
</Grid.RowDefinitions>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="auto"/>
<ColumnDefinition Width="*"/>
</Grid.ColumnDefinitions>
<Ellipse Grid.Column="0" Grid.Row="0" Height="32" Width="32"
VerticalAlignment="Top" HorizontalAlignment="Left">
<Ellipse.Fill>
<ImageBrush ImageSource="{Binding IconPathName}" />
</Ellipse.Fill>
</Ellipse>
<TextBlock Grid.Column="1" Grid.Row="0" TextWrapping="WrapWithOverflow"
HorizontalAlignment="Left" VerticalAlignment="Top"
Text="{Binding Name}" />
</Grid>
</DataTemplate>
</ListView.ItemTemplate>
</ListView>
BTW: I already did a work around where I am getting the Index from each ListViewItem and calculating against the Width of the Grid inside the DataTemplate that is a fixed value of 180, but unfortunately it did not work as I expected since I had to use a DependencyProperty to bind the ActualWidth of the of the ListView to my ViewModel and did not responded very well when I resized the window.
I know I am looking for a very particular behavior, but if anyone has any suggestions about how to deal with this I would really appreciate. Any thoughts are welcome even if you think I should be using a different control, please detail.
Thanks in advance!
You shouldn't handle the layout in any view model. If you didn't extend ListView consider to use an attached behavior (raw example):
ListBox.cs
public class ListBox : DependencyObject
{
#region IsAlternateFirstRowTemplateEnabled attached property
public static readonly DependencyProperty IsAlternateFirstRowTemplateEnabledProperty = DependencyProperty.RegisterAttached(
"IsAlternateFirstRowTemplateEnabled",
typeof(bool), typeof(ListView),
new PropertyMetadata(default(bool), ListBox.OnIsEnabledChanged));
public static void SetIsAlternateFirstRowTemplateEnabled(DependencyObject attachingElement, bool value) => attachingElement.SetValue(ListBox.IsAlternateFirstRowTemplateEnabledProperty, value);
public static bool GetIsAlternateFirstRowTemplateEnabled(DependencyObject attachingElement) => (bool)attachingElement.GetValue(ListBox.IsAlternateFirstRowTemplateEnabledProperty);
#endregion
private static void OnIsEnabledChanged(DependencyObject attachingElement, DependencyPropertyChangedEventArgs e)
{
if (!(attachingElement is System.Windows.Controls.ListBox listBox))
{
return;
}
if ((bool)e.NewValue)
{
listBox.Loaded += ListBox.Initialize;
}
else
{
listBox.SizeChanged -= ListBox.OnListBoxSizeChanged;
}
}
private static void Initialize(object sender, RoutedEventArgs e)
{
var listBox = sender as System.Windows.Controls.ListBox;
listBox.Loaded -= ListBox.Initialize;
// Check if items panel is WrapPanel
if (!listBox.TryFindVisualChildElement(out WrapPanel panel))
{
return;
}
listBox.SizeChanged += ListBox.OnListBoxSizeChanged;
ListBox.ApplyFirstRowDataTemplate(listBox);
}
private static void OnListBoxSizeChanged(object sender, SizeChangedEventArgs e)
{
if (!e.WidthChanged)
{
return;
}
var listBox = sender as System.Windows.Controls.ListBox;
ListBox.ApplyFirstRowDataTemplate(listBox);
}
private static void ApplyFirstRowDataTemplate(System.Windows.Controls.ListBox listBox)
{
double calculatedFirstRowWidth = 0;
var firstRowDataTemplate = listBox.Resources["FirstRowDataTemplate"] as DataTemplate;
foreach (FrameworkElement itemContainer in listBox.ItemContainerGenerator.Items
.Select(listBox.ItemContainerGenerator.ContainerFromItem).Cast<FrameworkElement>())
{
calculatedFirstRowWidth += itemContainer.ActualWidth;
if (itemContainer.TryFindVisualChildElement(out ContentPresenter contentPresenter))
{
if (calculatedFirstRowWidth > listBox.ActualWidth - listBox.Padding.Right - listBox.Padding.Left)
{
if (contentPresenter.ContentTemplate == firstRowDataTemplate)
{
// Restore the default template of previous first row items
contentPresenter.ContentTemplate = listBox.ItemTemplate;
continue;
}
break;
}
contentPresenter.ContentTemplate = firstRowDataTemplate;
}
}
}
}
Helper Extension Method
/// <summary>
/// Traverses the visual tree towards the leafs until an element with a matching element type is found.
/// </summary>
/// <typeparam name="TChild">The type the visual child must match.</typeparam>
/// <param name="parent"></param>
/// <param name="resultElement"></param>
/// <returns></returns>
public static bool TryFindVisualChildElement<TChild>(this DependencyObject parent, out TChild resultElement)
where TChild : DependencyObject
{
resultElement = null;
if (parent is Popup popup)
{
parent = popup.Child;
if (parent == null)
{
return false;
}
}
for (var childIndex = 0; childIndex < VisualTreeHelper.GetChildrenCount(parent); childIndex++)
{
DependencyObject childElement = VisualTreeHelper.GetChild(parent, childIndex);
if (childElement is TChild child)
{
resultElement = child;
return true;
}
if (childElement.TryFindVisualChildElement(out resultElement))
{
return true;
}
}
return false;
}
Usage
<ListView x:Name="lv"
ListBox.IsAlternateFirstRowTemplateEnabled="True"
ItemsSource="{Binding Path = ItemsSource}"
SelectedItem="{Binding Path = SelectedItem}">
<ListView.ItemsPanel>
<ItemsPanelTemplate>
<WrapPanel Orientation="Horizontal" />
</ItemsPanelTemplate>
</ListView.ItemsPanel>
<ListView.Resources>
<DataTemplate x:Key="FirstRowDataTemplate">
<!-- Draw a red border around first row items -->
<Border BorderThickness="2" BorderBrush="Red">
<Grid Width="180" Height="35">
<Grid.RowDefinitions>
<RowDefinition />
</Grid.RowDefinitions>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="auto"/>
<ColumnDefinition Width="*"/>
</Grid.ColumnDefinitions>
<Ellipse Grid.Column="0" Grid.Row="0" Height="32" Width="32"
VerticalAlignment="Top" HorizontalAlignment="Left">
<Ellipse.Fill>
<ImageBrush ImageSource="{Binding IconPathName}" />
</Ellipse.Fill>
</Ellipse>
<TextBlock Grid.Column="1" Grid.Row="0" TextWrapping="WrapWithOverflow"
HorizontalAlignment="Left" VerticalAlignment="Top"
Text="{Binding Name}" />
</Grid>
</Border>
</DataTemplate>
</ListView.Resources>
<ListView.ItemTemplate>
<DataTemplate>
<Grid Width="180" Height="35">
<Grid.RowDefinitions>
<RowDefinition />
</Grid.RowDefinitions>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="auto"/>
<ColumnDefinition Width="*"/>
</Grid.ColumnDefinitions>
<Ellipse Grid.Column="0" Grid.Row="0" Height="32" Width="32"
VerticalAlignment="Top" HorizontalAlignment="Left">
<Ellipse.Fill>
<ImageBrush ImageSource="{Binding IconPathName}" />
</Ellipse.Fill>
</Ellipse>
<TextBlock Grid.Column="1" Grid.Row="0" TextWrapping="WrapWithOverflow"
HorizontalAlignment="Left" VerticalAlignment="Top"
Text="{Binding Name}" />
</Grid>
</DataTemplate>
</ListView.ItemTemplate>
</ListView>
Remarks
If the visual tree itself will not change for the first row, consider to add a second attached property to the ListBox class (e.g., IsFirstRowItem) which you would set on the ListBoxItems. You can then use a DataTrigger to modify the control properties to change the appearance. This will very likely increase the performance too.

How to get a element's position which in a UserControl

I have a UserControl:
<UserControl d:DesignHeight="100" d:DesignWidth="200" ...>
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="100" />
<ColumnDefinition Width="100" />
</Grid.ColumnDefinitions>
<Ellipse Name="leftEllipse" Grid.Column="0" Width="50" Height="50" HorizontalAlignment="Center" VerticalAlignment="Center" Fill="Red" />
<Ellipse Name="rightEllipse" Grid.Column="1" Width="50" Height="50" HorizontalAlignment="Center" VerticalAlignment="Center" Fill="Green" />
</Grid>
</UserControl>
Here is my MainWindow:
<Window ...>
<Canvas Name="canvas1">
<my:MyUserControl x:Name="myUserControl1" Width="200" Height="100" Canvas.Top="100" Canvas.Left="100" />
</Canvas>
</Window>
I know how to get the position of myUserControl1 :
double x = Canvas.GetLeft(myUserControl1);
But can anyone tell me how to get the position of myUserControl1.leftEllipse?
And when myUserControl1 apply a RotateTransform, the myUserControl1.leftEllipse's position will changed, won't it?
Without making the generated leftEllipse field public, you could add a method to the UserControl that returns a transform object from the Ellipse's coordinates to that of an ancestor element, e.g.
public GeneralTransform LeftEllipseTransform(UIElement e)
{
return leftEllipse.TransformToAncestor(e);
}
You may then call it in your MainWindow like this:
var p = myUserControl1.LeftEllipseTransform(this).Transform(new Point());
Instead of TransformToAncestor (or TransformToVisual) you may also use TranslatePoint.
public Point GetLeftEllipsePosition(Point p, UIElement e)
{
return leftEllipse.TranslatePoint(p, e);
}
In MainWindow:
var p = myUserControl1.GetLeftEllipsePosition(new Point(), this);
Or for the center of the Ellipse (instead of its upper left corner):
var p = myUserControl1.GetLeftEllipsePosition(new Point(25, 25), this);

Textbox binding to list won't update

HI,
I have a list of coursesTaken with dates (returned by a nested class). Since I am not sure howmany courses the person took, I just bind it to textboxes and is working fine. The datacontext of the grid is the list itself so I assume whenever I make changes to the form, it is automatically sent back to the list and add it, but when it is time to save it in DB, the list won't change. I tried making the list as an observable collection and bind mode two way but still won't change. when I try to put Onpropertychange it says "Cannot acces a non static member of outer type via nested type..." What I want to do is whenever I type / add something to textbox, the list will add it as it's nth item so I can iterate and save it in DB.
is ther any other way to do this? please see code below. thanks!
#region class for binding purposes (nested class)
public class ListCon
{
public ObservableCollection<tblGALContinuingEducationHistory> EducList
{
get
{
return new ObservableCollection<tblGALContinuingEducationHistory>(contEducHstoryList);
}
}
}
#endregion
#region constructor
public ContinuingEducHistoryPopUp(tblAttorneyGalFileMaint currentGal)
{
contEducHstoryList = new ObservableCollection<tblGALContinuingEducationHistory>();
InitializeComponent();
if (currentGal != null)
{
CurrentGal = currentGal;
txtMemberName.Text = CurrentGal.FullName;
contEducHstoryList = new ObservableCollection<tblGALContinuingEducationHistory>(currentGal.tblGALContinuingEducationHistories.Count > 0 && currentGal.tblGALContinuingEducationHistories != null ? currentGal.tblGALContinuingEducationHistories : null);
}
this.DataContext = new ListCon();
}
#endregion
Now here's my xaml
<Grid Grid.Row="2" Grid.ColumnSpan="2" Name="grdEducForm">
<Grid.RowDefinitions>
<RowDefinition Height="25" />
<RowDefinition Height="25" />
<RowDefinition Height="25" />
<RowDefinition Height="25" />
<RowDefinition Height="25" />
</Grid.RowDefinitions>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="2*" />
<ColumnDefinition Width="*" />
<ColumnDefinition Width="*" />
</Grid.ColumnDefinitions>
<TextBox Text="{Binding Path=EducList[0].CourseTitle}" Grid.Column="0" Grid.Row="0" />
<useable:MaskedDatePicker DateValue="{Binding Path=EducList[0].CertificateDate}" Grid.Column="1" Grid.Row="0" />
<useable:MaskedDatePicker DateValue="{Binding Path=EducList[0].CertificateExpiration}" Grid.Column="2" Grid.Row="0" />
<TextBox Text="{Binding EducList[1].CourseTitle}" Grid.Column="0" Grid.Row="1" />
<useable:MaskedDatePicker DateValue="{Binding Path=EducList[1].CertificateDate}" Grid.Column="1" Grid.Row="1" />
<useable:MaskedDatePicker DateValue="{Binding Path=EducList[1].CertificateExpiration}" Grid.Column="2" Grid.Row="1" />
<TextBox Text="{Binding Path=EducList[2].CourseTitle}" Grid.Column="0" Grid.Row="2" />
<useable:MaskedDatePicker DateValue="{Binding Path=EducList[2].CertificateDate}" Grid.Column="1" Grid.Row="2" />
<useable:MaskedDatePicker DateValue="{Binding Path=EducList[2].CertificateExpiration}" x:Name="dpEnd3"
Grid.Column="2" Grid.Row="2" />
<TextBox Text="{Binding Path=EducList[3].CourseTitle, Mode=TwoWay}" Grid.Column="0" Grid.Row="3" />
<useable:MaskedDatePicker DateValue="{Binding Path=EducList[3].CertificateDate, Mode=TwoWay}" Grid.Column="1"
Grid.Row="3" />
<useable:MaskedDatePicker DateValue="{Binding Path=EducList[3].CertificateExpiration}" Grid.Column="2" Grid.Row="3" />
<TextBox Text="{Binding Path=EducList[4].CourseTitle}" Name="txtEducName5" Grid.Column="0" Grid.Row="4" />
<useable:MaskedDatePicker DateValue="{Binding Path=EducList[4].CertificateDate}" x:Name="dpStart5" Grid.Column="1"
Grid.Row="4" />
<useable:MaskedDatePicker DateValue="{Binding Path=EducList[4].CertificateExpiration}" x:Name="dpEnd5"
Grid.Column="2" Grid.Row="4" />
</Grid>
In your ListCon property you always return a new collection this defeats the point of having an ObservableCollection at all, you should store it in a field like this:
//Initialize field in constructor
private readonly ObservableCollection<tblGALContinuingEducationHistory> _ListCon;
public ObservableCollection<tblGALContinuingEducationHistory> ListCon
{
get
{
return _ListCon;
}
}
EducList = new ObservableCollection<tblGALContinuingEducationHistory>(currentGal.tblGALContinuingEducationHistories.Count > 0 && currentGal.tblGALContinuingEducationHistories != null ? currentGal.tblGALContinuingEducationHistories : null);
int count = 0;
//5 is the maximum no of course an atty can take and save in this form
count = 5 - EducList.Count;
for (int i = 0; i < count; i++)
{
galContEdu = FileMaintenanceBusiness.Instance.Create<tblGALContinuingEducationHistory>();
galContEdu.AttorneyID = currentGal.AttorneyID;
EducList.Add(galContEdu);
}
then save only the ones that has data:
foreach (var item in EducList)
{
if(!string.IsNullOrEmpty(item.CourseTitle) || !string.IsNullOrWhiteSpace(item.CourseTitle))
FileMaintenanceBusiness.Instance.SaveChanges(item, true);
}

Silverlight 4: Pattern for displaying data with a bit of logic?

I'm building a wp7 app.
I have a UserControl that displays a news article headline, teaser, and image. The entire class is pretty short:
public partial class StoryControl : UserControl
{
public Story Story { get; private set; }
public StoryControl()
{
InitializeComponent();
}
internal StoryControl(Story story) : this()
{
this.Story = story;
Teaser.Text = story.Teaser;
Headline.Text = story.Title;
if (story.ImageSrc == null)
{
Thumbnail.Visibility = Visibility.Collapsed;
} else
{
Thumbnail.Source = new BitmapImage(story.ImageSrc);
}
}
}
And the corresponding XAML:
<Grid x:Name="LayoutRoot" Background="Transparent" Margin="0,0,0,20">
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
<RowDefinition Height="*" />
</Grid.RowDefinitions>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto" />
<ColumnDefinition Width="*" />
</Grid.ColumnDefinitions>
<Image x:Name="Thumbnail" Grid.Column="0" Width="89" HorizontalAlignment="Left" VerticalAlignment="Top" />
<!-- sometimes there's a hanging word in the headline that looks a bit awkward -->
<TextBlock x:Name="Headline" Grid.Column="1" Grid.Row="0" Style="{StaticResource PhoneTextAccentStyle}" TextWrapping="Wrap" HorizontalAlignment="Left" FontSize="23.333" VerticalAlignment="Top" />
<TextBlock x:Name="Teaser" Grid.Column="0" Grid.ColumnSpan="2" Grid.Row="1" HorizontalAlignment="Left" Style="{StaticResource PhoneTextSubtleStyle}" TextWrapping="Wrap" VerticalAlignment="Top" Width="384"/>
</Grid>
Is there a way to do with with less code-behind and more XAML? Some way to use binding to bind the text for Headline and Teaser to properties of the Story, while not crashing if Story is null?
About about the image? I've got a bit of logic there; is there some way to automatically do that in XAML, or am I stuck with that in C#?
Looks like a ViewModel is in order:
public class StoryViewModel
{
readonly Story story;
public StoryViewModel(Story story)
{
this.story = story;
}
public string Teaser { get { return story == null ? "" : story.Teaser; } }
public string Title { get { return story == null ? "" : story.Title; } }
public bool IsThumbnailVisible { get { return story != null && story.ImageSrc != null; } }
public BitmapImage Thumbnail { get { return IsThumbnailVisible ? new BitmapImage(story.ImageSrc) : null; } }
}
Making your codebehind nice and simple:
public partial class StoryControl : UserControl
{
public Story Story { get; private set; }
public StoryControl()
{
InitializeComponent();
}
internal StoryControl(Story story)
: this()
{
this.DataContext = new StoryViewModel(story);
}
}
And your XAML becomes a set of bindings:
<Grid x:Name="LayoutRoot" Background="Transparent" Margin="0,0,0,20">
<Grid.Resources>
<BooleanToVisibilityConverter x:Key="booleanToVisiblityConverter"/>
</Grid.Resources>
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
<RowDefinition Height="*" />
</Grid.RowDefinitions>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto" />
<ColumnDefinition Width="*" />
</Grid.ColumnDefinitions>
<Image Visibility="{Binding IsThumbnailVisible, Converter={StaticResource booleanToVisiblityConverter}}" Source="{Binding Thumbnail}" Grid.Column="0" Width="89" HorizontalAlignment="Left" VerticalAlignment="Top" />
<!-- sometimes there's a hanging word in the headline that looks a bit awkward -->
<TextBlock Text="{Binding Title}" Grid.Column="1" Grid.Row="0" Style="{StaticResource PhoneTextAccentStyle}" TextWrapping="Wrap" HorizontalAlignment="Left" FontSize="23.333" VerticalAlignment="Top" />
<TextBlock Text="{Binding Teaser}" Grid.Column="0" Grid.ColumnSpan="2" Grid.Row="1" HorizontalAlignment="Left" Style="{StaticResource PhoneTextSubtleStyle}" TextWrapping="Wrap" VerticalAlignment="Top" Width="384"/>
</Grid>
Ok, it would probably be possible to do this with just model (story) and view (xaml) via fallbackvalues and more complex converters, but i hope you'll find that viewmodels give you the most power in terms of testable, brick-wall-less, view-specific logic...

Resources