资源
功能和要求
界面设计
参考后面的截图,要求用到Grid(RowDefinitions、ColumnDefinitions)、 ScrollViewer、 StackPanel、BottomAppBar、RelativePannel、Image、TextBlock、DatePicker、APPBarButton。
自适应效果:当窗口宽度小于800时,只显示原本在界面左侧的列表部分,底部导航栏只显示Add图标;窗口宽度大于800时显示完整界面。窗口宽度小于600时,列表项中的图片不显示。(第二周验收只要求在界面宽度发生改变时,界面整体始终居中)
两块界面的右侧均需有滚动条。
页面的导航与跳转
点击MainPage底部的“+”按钮,跳转到NewPage;点击NewPage顶部的“←”按钮,跳转回MainPage。而在宽屏显示两块的情况下,点击“+”则无需跳转。窄屏状态下删除或添加后要跳回Mainpage界面。(第二周不要求宽屏情况)
页面内容
列表的每一项包括复选框、图片、文字(标题)。当复选框被勾选时有划掉的横线出现,取消勾选则横线消失。点击某一项,能跳转到详情页或者是宽屏状态下显示在右侧。详情页除了刚才的三个信息还有详情和日期,另有一个调节图片大小的滑块和两个按钮。
增删改
点击底部“+”或宽屏状态点击create新建,新建时检查Title、Description是否为空,DueDate是否正确(是否大于等于当前日期)。如果不正确,弹出对话框,显示错误信息。
在详情页点击update按钮可以修改信息。
新建中点击Cancel按钮,Title、Description置空,DueDate置为当前日期;修改中点击Cancel按钮,Title、Description、DueDate还原该清单项的详情数据。
选中一个清单项,点击底部删除图标可以删除该项。
主要解决问题
- 项目的误区
- 页面不同宽度自适应
- ListView数据绑定
- 不同页面跳转传值
- 本地文件的图片选择以及绑定
项目的误区
确定newpage和mainpage的关系,在宽屏的时候不是newpage和mainpage合成了一个新的page,而是mainpage自身就是一个复杂的页面,既包含mainpage原本的内容,也要加入newpage那一部分,所以newpage和mainpage中的newpage那一部分不是同一个部分。
页面不同宽度自适应
这个本来可以在blend for visual studio中点击就可以完成,但是不推荐这种方法,一般来说blend for visual studio提供的功能没有代码多。第二因为涉及到listview所以与一般的自适应有些许不同,mainpage左边的部分是listView部分,是与右边分开的,如果VisualStateManager是在mainpage外面写的,则会造成,listView内部的图片查不到,明明已经写了但是是查不到的,可能有用UserControl解决的方法,但是我没有试出来。如果是在listView内部写VisualStateManager,那么又控制不到mainpage右边的部分,所以最好的方法是两个都写,一个在listView外面,一个在listView里面。
在ListView外面写入如下代码,当宽度大于800时,grid2也就是右边页面可见
<VisualStateManager.VisualStateGroups>
<VisualStateGroup x:Name="VisualStateGroup">
<VisualState x:Name="VisualState800">
<VisualState.Setters>
<Setter Target="grid2.(UIElement.Visibility)" Value="Visible"/>
</VisualState.Setters>
<VisualState.StateTriggers>
<AdaptiveTrigger MinWindowWidth="800"/>
</VisualState.StateTriggers>
</VisualState>
</VisualStateGroup>
</VisualStateManager.VisualStateGroups>
在listView里面写入如下代码,当宽度在600和800之间,使得图片可见
<VisualStateManager.VisualStateGroups>
<VisualStateGroup x:Name="VisualStateGroup">
<VisualState x:Name="VisualState600">
<VisualState.Setters>
<Setter Target="Image.(UIElement.Visibility)" Value="Visible"/>
</VisualState.Setters>
<VisualState.StateTriggers>
<AdaptiveTrigger MinWindowWidth="600"/>
</VisualState.StateTriggers>
</VisualState>
</VisualStateGroup>
</VisualStateManager.VisualStateGroups>
ListView数据绑定
文件结构
首先要新建文件夹Models,在里面新建c#文件ListItem.cs,再新建文件夹ViewModels,在里面新建c#文件ListItemViewModels.cs。项目采用mvvm模式,ListItem.cs主要是申明数据结构,ListItemViewModels.cs是ListItem这个类实例的集合,用来删除,更新,新建操作。
ListItem.cs
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.ComponentModel;
using Windows.UI.Xaml.Media.Imaging;
namespace first_project.Models
{
public class ListItem : INotifyPropertyChanged
{
public event PropertyChangedEventHandler PropertyChanged;
public string id;
public string title { get; set; }
public string description { get; set; }
public DateTimeOffset date { get; set; }
public bool completed { get; set; }
public BitmapImage src { get; set; }
public bool Completed
{
get { return this.completed; }
set
{
this.completed = value;
NotifyPropertyChanged("Completed");
}
}
public void NotifyPropertyChanged(string propertyName)
{
if (PropertyChanged != null)
{
PropertyChanged(this, new PropertyChangedEventArgs(propertyName));
}
}
public ListItem()
{
}
public ListItem(string title,string description ,DateTimeOffset date,BitmapImage input)
{
this.id = Guid.NewGuid().ToString();
this.title = title;
this.description = description;
this.completed = false;
this.date = date;
this.src = input;
}
}
}
各个变量的作用以及继承类的作用
- id是标识某个ListItem,用系统自带API生成
- title,description,date不必多说,是某个ListItem的基本属性
- completed这个属性,用到的地方是checkbox和Line绑定的时候
- src是图片,主要是做本地图片选择的时候用到,如果不做这个功能可以不用
- 有一个ListItem()构造函数,为了后面临时生成一个对象,所以没有处理
- 继承NotifyPropertyChanged这个类,说明是向客户端发出某一属性值已更改的通知,具体是什么作用暂时还没懂,但是绑定checkbox和line时候会有用。
ListItemViewModels.cs
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Collections.ObjectModel;
using first_project.Models;
using Windows.UI.Xaml.Media.Imaging;
namespace first_project.ViewModels
{
public class ListItemViewModels
{
private ObservableCollection<Models.ListItem> allItems = new ObservableCollection<Models.ListItem>();
public ObservableCollection<Models.ListItem> AllItems { get { return this.allItems; } }
public void AddTodoItem(string title,string description,DateTimeOffset date,BitmapImage src)
{
this.allItems.Add(new Models.ListItem(title, description,date,src));
}
public void RemoveTodoItem(string id)
{
ListItem temp = new ListItem();
for(int i = 0; i < allItems.Count(); i++)
{
if(allItems[i].id == id)
{
temp = allItems[i];
break;
}
}
this.allItems.Remove(temp);
}
public void UpdateTodoItem(string id,string title,string description,DateTimeOffset date,BitmapImage src)
{
/*ListItem temp = new ListItem();
for (int i = 0; i < allItems.Count(); i++)
{
if (allItems[i].id == id)
{
allItems[i].title = title;
allItems[i].description = description;
allItems[i].date = date;
allItems[i].src = src;
break;
}
}*/
this.RemoveTodoItem(id);
this.AddTodoItem(title, description, date, src);
}
}
}
关于ListItemViewModels的说明
- 用了ObservableCollection方法
- RemoveTodoItem中临时新建了一个ListItem对象,然后for循环查到删除之,不知道有没有更好的方法,直接通过id删除等,有removeAt函数,但是要求是int型的,而string id,是一串很长的码,不可能转成int型的。
- UpdateTodoItem注释掉了一部分,如果不考虑图片的话,那部分是没问题的,但是加了图片之后,图片并没有更新,所以注释掉,先删除再创建。暂时没查明问题。
MainPage中ListView
<ListView IsItemClickEnabled="True" ItemClick="TodoTtem_ItemClicked" ItemsSource = "{x:Bind ViewModel.AllItems}" >
<ListView.ItemTemplate>
<DataTemplate x:DataType="md:ListItem">
<UserControl>
<Grid x:Name="GridLeft">
<VisualStateManager.VisualStateGroups>
<VisualStateGroup x:Name="VisualStateGroup">
<VisualState x:Name="VisualState600">
<VisualState.Setters>
<Setter Target="Image.(UIElement.Visibility)" Value="Visible"/>
</VisualState.Setters>
<VisualState.StateTriggers>
<AdaptiveTrigger MinWindowWidth="600"/>
</VisualState.StateTriggers>
</VisualState>
</VisualStateGroup>
</VisualStateManager.VisualStateGroups>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="30" />
<ColumnDefinition Width="*" />
<ColumnDefinition Width="*" />
<ColumnDefinition Width="Auto"/>
</Grid.ColumnDefinitions>
<CheckBox Grid.Column="0" HorizontalAlignment="Center" x:Name="box" Height="42" Width="81" Grid.ColumnSpan="2" Margin="0,4,11,4" IsChecked="{x:Bind Path=Completed, Converter={StaticResource CheckBoxConverter},Mode=TwoWay}"/>
<Image Grid.Column="1" x:Name="Image" HorizontalAlignment="Center" Source="{x:Bind src}" Width="50" Visibility="Collapsed" />
<TextBlock Grid.Column="2" HorizontalAlignment="Left" Text="{x:Bind title}" VerticalAlignment="Center" Height="20" Width="60" />
<Line x:Name="Line" Grid.Column="2" Stretch="Fill" Stroke="Black" StrokeThickness="2" Visibility="{x:Bind Path=Completed, Converter={StaticResource LineConverter},Mode=OneWay}" X1="1" Grid.ColumnSpan="2" />
</Grid>
</UserControl>
</DataTemplate>
</ListView.ItemTemplate>
</ListView>
关于ListView的说明
- ItemClick=”TodoTtem_ItemClicked”这个函数,实现的功能是如果是宽度在800以上,则点击屏幕下方的加号,不会跳转新页面,直接将相关的信息显示在右侧,如果是小于宽度800,则跳转到NewPage界面并显示数据。
- ItemsSource = “{x:Bind ViewModel.AllItems}” 这里的ViewModel经常会找不到,个人觉得是和MainPage.cs中是否在MainPage()构造函数中实例化有关
ViewModel = new ListItemViewModels();
- Checkbox的绑定,Checkbox本来是要和line绑定的,但是在ListView里面,和平时有不一样,无法绑定到Line,于是借助中间变量completed,将Checkbox IsChecked与Completed做绑定,用了双向绑定,
IsChecked="{x:Bind Path=Completed, Converter={StaticResource CheckBoxConverter}, Mode=TwoWay}
,因为需要改变数据completed的值,当checkbox为勾选状态使得completed为正,虽然都是bool类型的值,但是不明白为什么不可以直接绑定,于是用转换器,使得可以绑定。 - Line的绑定,Line也和Completed绑定,这个只要单向绑定即可,completed的值为真的时候Line的Visibility属性就为Visible,否则为Collapsed。这个地方做了很久都没有反应,最后试了ListItem继承INotifyPropertyChanged类,并补全相关代码,成功了。
Image和TextBlock直接绑定即可,无需转换,其实我对于Image还是有疑问的,因为Image的Source和src类型不匹配,src是BitmapImage类型,而它的地址是UriSource,所以不明白。
converter.cs
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using Windows.UI.Xaml.Data;
using Windows.UI.Xaml;
namespace first_project
{
public class CheckBoxConverter : IValueConverter
{
public object Convert(object value, Type targetType, object parameter, string language)
{
bool temp = (bool)value;
if(temp == true)
{
return true;
}
else
{
return false;
}
}
public object ConvertBack(object value, Type targetType, object parameter, string language)
{
return (bool)value;
}
}
public class LineConverter : IValueConverter
{
public object Convert(object value, Type targetType, object parameter, string language)
{
bool temp = (bool)value;
if (temp)
return Visibility.Visible;
return Visibility.Collapsed;
}
public object ConvertBack(object value, Type targetType, object parameter, string language)
{
throw new NotImplementedException();
}
}
}
- 同样继承一个类IValueConverter,原因不知道
剩下的应该不难看懂
不同页面跳转传值
应该是有用全局变量的方法,但这里用navigateTo的方法
contenet.cs
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using first_project.ViewModels;
using first_project.Models;
namespace first_project
{
public class Content
{
public string id { get; set; }
public ListItemViewModels result { get; set; }
public Content()
{
id = "";
}
public Content(string id,ListItemViewModels input)
{
this.id = id;
this.result = input;
}
}
}
content.cs的作用
个人觉得有点冗余,但是也是因为没有找到好的解决方案,因为一开始考虑的是将数据全都传到第二个页面,如果第二个页面返回,肯定也要将所有数据再传回来,所以肯定不能只传一个ListItem,而要传allItems,但是只传allItems,就不知道点击的是哪一个,所以还要穿id,于是将两个整合成一个类,传递到第二个页面,应该有更好的方法。
点击ListItem传递的代码
public void TodoTtem_ItemClicked(object sender, ItemClickEventArgs e)
{
var temp = (ListItem)e.ClickedItem;
currentId = temp.id;
if (Window.Current.Bounds.Width > 800)
{
Create_To_Update();
Delete.Visibility = Visibility.Visible;
TitleBlock.Text = temp.title;
DetailBlock.Text = temp.description;
Date.Date = temp.date;
NewImage.Source = temp.src;
}
else
{
Content result = new Content(currentId, ViewModel);
this.Frame.Navigate(typeof(NewPage), result);
}
实际上有作用的就`this.Frame.Navigate(typeof(NewPage), result);这一句,将result传到NewPage页面。
点击加号传递的代码
private void addBarClick(object sender, RoutedEventArgs e)
{
if(isClick == true)
{
Content result = new Content("NULL", ViewModel);
this.Frame.Navigate(typeof(NewPage),result);
}
}
将id值设为NULL,为了接受时判读做准备
接受的代码
protected override void OnNavigatedTo(NavigationEventArgs e)
{
base.OnNavigatedTo(e);
temp = (Content)e.Parameter;
if(temp.result.AllItems.Count() > 0 && temp.id != "NULL")
{
Create_To_Update();
ListItem result = new ListItem();
for (int i = 0; i < temp.result.AllItems.Count(); i++)
{
if (temp.id == temp.result.AllItems[i].id)
{
result = temp.result.AllItems[i];
break;
}
}
TitleBlock.Text = result.title;
DetailBlock.Text = result.description;
Date.Date = result.date;
NewImage.Source = result.src;
}
else
{
Update_To_Create();
}
}
首先判断是点页面下面加号跳转的,还是点击某个ListView跳转的,用的就是判断temp.id是否是NULL,依然是临时变量找实例。
页面返回时数据消失的解决
本来考虑的是用NewPage传值给MainPage,这就要在MainPage中写NavigateTo函数,但是实际上很难行得通,因为程序一开始运行就要进入MainPage页面,但那个时候没有任何值传给MainPage,所以就会报错,于是采用其他方法,用NavigationCache,即使传递了值,MainPage中任然会有值,代码也比较简单,只要在MainPage的构造函数中加上NavigationCacheMode = NavigationCacheMode.Enabled;
就可以了。
本地文件的图片选择以及绑定
本地图片选择代码
private async void Select_Click(object sender, RoutedEventArgs e)
{
//文件选择器
FileOpenPicker openPicker = new FileOpenPicker();
//选择视图模式
openPicker.ViewMode = PickerViewMode.Thumbnail;
//openPicker.ViewMode = PickerViewMode.List;
//初始位置
openPicker.SuggestedStartLocation = PickerLocationId.PicturesLibrary;
//添加文件类型
openPicker.FileTypeFilter.Add(".jpg");
openPicker.FileTypeFilter.Add(".jpeg");
openPicker.FileTypeFilter.Add(".png");
StorageFile file = await openPicker.PickSingleFileAsync();
if (file != null)
{
using (IRandomAccessStream stream = await file.OpenAsync(FileAccessMode.Read))
{
var srcImage = new BitmapImage();
await srcImage.SetSourceAsync(stream);
NewImage.Source = srcImage;
}
}
}
这个网上基本上都能找到,问题在于最后一步NewImage.Source = srcImage;
我不是很清楚srcImage的类型。本来是没有图片的,后来加上这个功能,从头到尾都添加 了BitmapImage类型的值,一开始是添加string类型的值保存ImageSource的地址,发现类型不一样,于是放弃,采用BitmapImage,但是这里有个问题,就是BitmapImage和Image类型不一样,于是去官网找了一下,产生了新的idea,用BitmapImage标签,代替Image标签,然后发现不行,最后还是决定将Image类型转换成BitmapImage,BitmapImage result = NewImage.Source as BitmapImage;
就这一句代码,解决了所有问题,使得NewImage.Source可以直接被BitmapImage的值赋值,于是就解决了所有问题。
建议和提示
- 很多找不到的东西,可能是没有加public,然后用点运算符的时候,系统没有显示里面的成员,误以为出了问题
- 记得加using,因为这个问题,我一般都给了代码的头文件引用,否则很麻烦