WPF使用 INotifyPropertyChanged 实现数据驱动
如下图,有这么一个常见需求,在修改表单明细的苹果价格时,总价会改变,同时单据总和也随之改变。
按照Winfrom事件驱动的思想来做的话,我们就需要在将UI的修改函数绑定到CellEdit事件中来实现。
但是对于WPF,我们完全可以利用WPF的 INotifyPropertyChanged 接口来实现。
首先我们通过nuget引入WPF常用的自动首先通知的第三方包 PropertyChanged.Fody ,它的作用是凡是实现了 INotifyPropertyChanged 的类的属性默认都会通知前端
然后建立订单和订单明细两个基本类,并实现 INotifyPropertyChanged 接口
public class DJ : INotifyPropertyChanged
{
public int ID { get; set; }
public double SumPrice
{
get
{
return MXs.Sum(it => it.Price);
}
}
public ObservableCollection<Models.DJMX> MXs { get; set; } = new ObservableCollection<DJMX>();
public event PropertyChangedEventHandler PropertyChanged;
}
public class DJMX : INotifyPropertyChanged
{
public object DJ { get; set; }
public object MainWindowViewModel { get; set; }
public string Name { get; set; }
private double price;
public double Price
{
get { return price; }
set
{
price = value;
}
}
public event PropertyChangedEventHandler PropertyChanged;
}
前端代码
<Grid>
<Grid.RowDefinitions>
<RowDefinition/>
<RowDefinition Height="30"/>
</Grid.RowDefinitions>
<DataGrid AutoGenerateColumns="False" CanUserAddRows="False" ItemsSource="{Binding DJs}">
<DataGrid.Columns>
<DataGridTextColumn Width="*" Header="订单号" Binding="{Binding ID}"/>
<DataGridTextColumn Width="*" Header="总价" Binding="{Binding SumPrice}"/>
</DataGrid.Columns>
<DataGrid.RowDetailsTemplate>
<DataTemplate>
<DataGrid AutoGenerateColumns="False" CanUserAddRows="False" SelectionUnit="CellOrRowHeader"
ItemsSource="{Binding MXs}">
<DataGrid.Columns>
<DataGridTextColumn Header="商品名" Width="100" Binding="{Binding Name}"/>
<DataGridTextColumn Header="价格" Width="100" Binding="{Binding Price, UpdateSourceTrigger=PropertyChanged}"/>
</DataGrid.Columns>
</DataGrid>
</DataTemplate>
</DataGrid.RowDetailsTemplate>
</DataGrid>
<StackPanel Grid.Row="1" VerticalAlignment="Center" Orientation="Horizontal">
<TextBlock Text="单据总和: "/>
<TextBlock Text="{Binding AllSumPrice}"/>
</StackPanel>
</Grid>
前端对应的ViewModel
public class MainWindowViewModel : INotifyPropertyChanged
{
public MainWindowViewModel()
{
DJs = new ObservableCollection<Models.DJ>()
{
new Models.DJ(){ ID=1},
new Models.DJ(){ ID=2},
new Models.DJ(){ ID=3},
new Models.DJ(){ ID=4},
new Models.DJ(){ ID=5}
};
foreach (var dj in DJs)
{
dj.MXs = new ObservableCollection<Models.DJMX>()
{
new Models.DJMX() { Name="苹果", Price=100 },
new Models.DJMX() { Name="鸭梨", Price=200 },
new Models.DJMX() { Name="香蕉", Price=300 },
};
}
}
public double AllSumPrice
{
get
{
return DJs.Sum(it => it.SumPrice);
}
}
public ObservableCollection<Models.DJ> DJs { get; set; } = new ObservableCollection<Models.DJ>();
public event PropertyChangedEventHandler PropertyChanged;
}
运行调试一下
发现价格修改并没有影响到总价和总和, 结果并不如预期的那样,我们分析一下:
来看总价和总和属性的定义,两个都是只读的,因为没有Set的属性,所以Fody是无法进行通知的,准确的说,是 PropertyChanged 没有设置到该属性。
例如,价格的属性代码完整其实是这样的
在价格属性改变后,会通过绑定价格属性的前端进行修改。
所以,如果我们想让价格修改的同时,总价和总和也要通知到,即可以在价格属性的Set方法中,增加通知 SumPrice 和 AllSumPrice 的代码。
而 PropertyChanged 需要传入一个当前属性所在的示例和当前属性的名称,在这里,我通过修改 OnPropertyChanged 增加一个 OnNavigationObjDJPropertyChanged 方法,
另外订单明细也需要定义两个新的obj属性用来存放需要通知的实例,达到类似EF导航属性的效果,最终的 DJMX 类代码如下
public class DJMX : INotifyPropertyChanged
{
public object DJ { get; set; }
public object MainWindowViewModel { get; set; }
public string Name { get; set; }
private double price;
public double Price
{
get { return price; }
set
{
price = value;
OnPropertyChanged(new PropertyChangedEventArgs("price"));
OnNavigationObjDJPropertyChanged(DJ, new PropertyChangedEventArgs("SumPrice")); //new
OnNavigationObjDJPropertyChanged(MainWindowViewModel, new PropertyChangedEventArgs("AllSumPrice")); //new
}
}
public event PropertyChangedEventHandler PropertyChanged;
public void OnPropertyChanged(PropertyChangedEventArgs e)
{
if (PropertyChanged != null)
{
PropertyChanged(this, e);
}
}
//new
public void OnNavigationObjDJPropertyChanged(object objTargert,PropertyChangedEventArgs e)
{
if (PropertyChanged != null&& objTargert!= null)
{
PropertyChanged(objTargert, e);
}
}
}
同时 ViewModel 的代码也需要在数据实例化时,增加传入两个通知的实例,代码如下:
public class MainWindowViewModel : INotifyPropertyChanged
{
public MainWindowViewModel()
{
DJs = new ObservableCollection<Models.DJ>()
{
new Models.DJ(){ ID=1},
new Models.DJ(){ ID=2},
new Models.DJ(){ ID=3},
new Models.DJ(){ ID=4},
new Models.DJ(){ ID=5}
};
foreach (var dj in DJs)
{
dj.MXs = new ObservableCollection<Models.DJMX>()
{
new Models.DJMX() { DJ=dj, MainWindowViewModel=this, Name="苹果", Price=100 }, //changed
new Models.DJMX() { DJ=dj, MainWindowViewModel=this, Name="鸭梨", Price=200 }, //changed
new Models.DJMX() { DJ=dj, MainWindowViewModel=this, Name="香蕉", Price=300 }, //changed
};
}
}
public double AllSumPrice
{
get
{
return DJs.Sum(it => it.SumPrice);
}
}
public ObservableCollection<Models.DJ> DJs { get; set; } = new ObservableCollection<Models.DJ>();
public event PropertyChangedEventHandler PropertyChanged;
}
我们再调试运行一次
完美!!!
2021年7月6日,对原代码进行扩展
现在有新需求,datagrid新增一“清单”的checkbox列,当订单的明细大于8时,清单明细为勾选状态,否则为非勾选状态。
如图所示
修改DJ类,新增IsQD字段用于绑定到datagrid清单列,代码如下
public class DJ : INotifyPropertyChanged
{
public int ID { get; set; }
public double SumPrice
{
get
{
return MXs.Sum(it => it.Price);
}
}
public bool IsQD
{
get
{
return MXs.Count > 8 ? true : false;
}
}
public ObservableCollection<Models.DJMX> MXs { get; set; } = new ObservableCollection<DJMX>();
public event PropertyChangedEventHandler PropertyChanged;
}
通过观察,我们可以发现,新加的IsQD同样也只有get方法,这次不同的是,IsQD的get方法的改变是因为订单明细 MXs 的Count属性改变而改变的。
所以同样我们需要添加对MXs的Count对IsQD的通知。
此操作在初始化MXs添加即可,核心代码如下
public MainWindowViewModel()
{
DJs = new ObservableCollection<Models.DJ>()
{
new Models.DJ(){ ID=1},
new Models.DJ(){ ID=2},
new Models.DJ(){ ID=3},
new Models.DJ(){ ID=4},
new Models.DJ(){ ID=5}
};
foreach (var dj in DJs)
{
dj.MXs = new ObservableCollection<Models.DJMX>()
{
new Models.DJMX() { DJ=dj, MainWindowViewModel=this, Name="苹果", Price=100 },
new Models.DJMX() { DJ=dj, MainWindowViewModel=this, Name="鸭梨", Price=200 },
new Models.DJMX() { DJ=dj, MainWindowViewModel=this, Name="香蕉", Price=300 },
new Models.DJMX() { DJ=dj, MainWindowViewModel=this, Name="苹果", Price=100 },
new Models.DJMX() { DJ=dj, MainWindowViewModel=this, Name="鸭梨", Price=200 },
new Models.DJMX() { DJ=dj, MainWindowViewModel=this, Name="香蕉", Price=300 },
new Models.DJMX() { DJ=dj, MainWindowViewModel=this, Name="苹果", Price=100 },
new Models.DJMX() { DJ=dj, MainWindowViewModel=this, Name="鸭梨", Price=200 },
new Models.DJMX() { DJ=dj, MainWindowViewModel=this, Name="香蕉", Price=300 },
};
dj.MXs.CollectionChanged += (sender, e) =>
{
if (PropertyChanged != null)
{
PropertyChanged(dj, new PropertyChangedEventArgs(nameof(Models.DJ.IsQD)));
}
};
}
}
.........
运行我们来看效果
嗯哼,似乎哪里不对
我们发现又有新的问题了,价格和总和并没有随着明细条数变化而变化。
因为我们在明细条数修改时,并有没有通知到这两个属性。知道问题原因后,我们添加新的代码
在调试运行一下:
运行完美!
翻译 朗读 复制 正在查询,请稍候…… 重试 朗读 复制 复制 朗读 复制 via 百度翻译 译
翻译 朗读 复制 正在查询,请稍候…… 重试 朗读 复制 复制 朗读 复制 via 百度翻译 译
翻译 朗读 复制 正在查询,请稍候…… 重试 朗读 复制 复制 朗读 复制 via 百度翻译 译
翻译 朗读 复制 正在查询,请稍候…… 重试 朗读 复制 复制 朗读 复制 via 百度翻译 译
翻译 朗读 复制 正在查询,请稍候…… 重试 朗读 复制 复制 朗读 复制 via 百度翻译 译
翻译 朗读 复制 正在查询,请稍候…… 重试 朗读 复制 复制 朗读 复制 via 百度翻译 译