2018年5月14日 星期一

React & ASP.NET Core 2.0 Upload File

一個新增檔案的Form含有上傳圖片的功能並可預覽圖片,圖片與表單內容一起傳送到後端,資料新增後再存檔圖片

建立一個選取圖片的子Component
import React from 'react';
 
class ImageUpload extends React.Component {
    constructor(props) {
        super(props);
        this.state = { file: '', imagePreviewUrl: '' };
    }
 
    handleImageChange(e) {
        e.preventDefault();
 
        let reader = new FileReader();
        let file = e.target.files[0];
 
        reader.onloadend = () => {
            this.setState({
                file: file,
                imagePreviewUrl: reader.result
            });
        };
 
        reader.readAsDataURL(file);
        this.props.onImageChange(file);
    }
 
    render() {
        let { imagePreviewUrl } = this.state;
        let $imagePreview = null;
        if (imagePreviewUrl) {
            $imagePreview = <img src={imagePreviewUrl} />;
        } else {
            $imagePreview = (
                <div className="previewText">
                    Please select an Image for Preview
                </div>
            );
        }
 
        return (
            <div className="previewComponent form-group">
                <label htmlFor="file">{this.props.label}</label>
                <input
                    id="file"
                    type="file"
                    className="fileInput form-control"
                    accept="image/*"
                    onChange={e => this.handleImageChange(e)}
                />
                <div className="imgPreview">
                    {$imagePreview}
                </div>
            </div>
        );
    }
}
 
ImageUpload.defaultProps = {
    label: '上傳圖片',
    onImageChange: (file) => {
        console.log('default onImageChange() has been called', file);
    }
};
 
export default ImageUpload;

建立新增資料的Form的父Component,兩個輸入欄位的type分別為text和file
import React from 'react';
import axios from 'axios';
 
import ImageUpload from './ImageUpload';
 
class CreateProduct extends React.Component {
    constructor(props) {
        super(props);
        this.state = {
            product: '',
            file: '',
        }
        this.handleSubmit = this.handleSubmit.bind(this);
        this.handleChange = this.handleChange.bind(this);
        this.handleImageChange = this.handleImageChange.bind(this);
    }
    handleSubmit(e) {
        e.preventDefault();
        const formData = new FormData();
        const keys = Object.keys(this.state);
        keys.forEach(key => {
            formData.append(key, this.state[key]);
        });
        const headers = { 'Content-Type''multipart/form-data' };
        axios.post('api/product', formData, { headers: headers }).then(response => {
            console.log('create success', response);
        });
    }
    handleChange(e) {
        this.setState({
            [e.target.id]: e.target.value
        });
    }
    handleImageChange(file) {
        this.setState({ file });
    }
    render() {
        const { product } = this.state;
        return (
            <div>
                <form onSubmit={this.handleSubmit}>
                    <div>
                        <label htmlFor="url">Product</label>
                        <input
                            type="text"
                            id="product"
                            value={product}
                            onChange={this.handleChange}
                        />
                    </div>
                    <ImageUpload onImageChange={this.handleImageChange} />
                    <button type="submit">Submit</button>
                </form>
            </div>
        );
    }
}
 
export default CreateProduct;

後端接收資料資料的參數類型
public class ProductCreationModel
{
    public string Product{ getset; }
    public IFormFile File { getset; }
}

處理上傳的後端程式
public class AppService
{
    private readonly IHostingEnvironment _environment;
 
    public AppService(IHostingEnvironment environment)
    {
        _environment = 
            environment ?? throw new ArgumentNullException(nameof(environment));
    }
    public async Task<string> Upload(IFormFile file, string name = null)
    {
        if (string.IsNullOrWhiteSpace(_environment.WebRootPath))
        {
            _environment.WebRootPath = Path.Combine(
                Directory.GetCurrentDirectory(), "wwwroot"
                );
        }
        var dir = Path.Combine(_environment.WebRootPath, "uploads");
        if (!Directory.Exists(dir))
        {
            Directory.CreateDirectory(dir);
        }
        var fileName = file.FileName;
        var path = Path.Combine(dir, fileName);
        if (!string.IsNullOrEmpty(name)) // 使用自訂的檔案名稱
        {
            var extension = Path.GetExtension(path);
            fileName = $"{name}{extension}";
            path = path.Replace(file.FileName, fileName);
        }
        if (file.Length > 0)
        {
            using (var fileStream = new FileStream(path, FileMode.Create))
            {
                try
                {
                    await file.CopyToAsync(fileStream);
                    return fileName;
                }
                catch (Exception ex)
                {
                    throw ex;
                }
            }
        }
        else
        {
            throw new Exception("file is empty");
        }
    }
}




2018年3月18日 星期日

範例:ASP.NET Core 2.0 DI & EntityFrameworkCore & Repository Pattern


假設建立了一個 DbContext 叫做 TempDbContext

    public class TempDbContext : DbContext
    {
       
public TempDbContext(DbContextOptions<TempDbContext> options)  : base(options)
        {

        }

       
public DbSet<Product> Products { get; set; }
    }

IRepository<T為一些常用操作(例如CRUD)的 interface
IProductRepository 為操作 Product 的 interface,並包含了IRepository<T>

   
public interface IRepository<T> where T : class
    {
       
IEnumerable<T> GetAll();
       
IEnumerable<T> Find(Func<T, bool> predicate);
       
T GetById(int id);
       
void Create(T entity);
       
void Update(T entity);
       
void Detele(T entity);
       
int Count(Func<T, bool> predicate);
    }

   
public interface IProductRepository : IRepository<Product>
    {
        //
一些操作 Product 的方法
       
Product GetProductByName(string name);
    }

實作 IRepository<T>

    public class Repository<T> : IRepository<T> where T : class
    {
       
// 會利用 ASP.NET Core 2.0 DI 注入實例
        protected readonly TempDbContext _context;
       
public Repository(TempDbContext context)
        {
            _context = context;
        }
       
public void Save() => _context.SaveChanges();

       
public int Count(Func<T, bool> predicate)
        {
           
return _context.Set<T>().Where(predicate).Count();
        }

       
public void Create(T entity)
        {
            _context.Add(entity);
            Save();
        }

       
public void Detele(T entity)
        {
            _context.Remove(entity);
            Save();
        }

       
public IEnumerable<T> Find(Func<T, bool> predicate)
        {
           
return _context.Set<T>().Where(predicate);
        }

       
public IEnumerable<T> GetAll()
        {
           
return _context.Set<T>();
        }

       
public T GetById(int id)
        {
           
return _context.Set<T>().Find(id);
        }

       
public void Update(T entity)
        {
            _context.Entry(entity).State =
EntityState.Modified;
            Save();
        }
    }

實作 IProductRepository,也繼承 Repository<Product>

    public class ProductRepository : Repository<Product>, IProductRepository
    {
       
public ProductRepository(TempDbContext context) : base(context)
        {
        }

       
public Product GetProductByName(string name)
        {
           
return _context
                .Products
                .Where(p => p.Name == name)
                .FirstOrDefault();
        }
    }

ASP.NET Core 2.0 DI 設定(Startup)

        public void ConfigureServices(IServiceCollection services)
        {
           
// Repository 建構子需要 DbContext
            services.AddDbContext<TempDbContext>(options => options.UseInMemoryDatabase("TempContext"));
            services.AddTransient<
IProductRepository, ProductRepository>();
            services.AddMvc();
        }

Controller 中使用 Repository

    [Route("api/[controller]")]
   
public class ProductController : Controller
    {
       
private readonly IProductRepository _productRepository;

       
public ProductController(IProductRepository productRepository)
        {
            _productRepository = productRepository;
        }
    }

==========
Debug:
在執行時發生了這個錯誤

Additional information: No database provider has been configured for this DbContext. A provider can be configured by overriding the DbContext.OnConfiguring method or by using AddDbContext on the application service provider. If AddDbContext is used, then also ensure that your DbContext type accepts a DbContextOptions object in its constructor and passes it to the base constructor for DbContext.
確定在ConfigureServices有設定DbContext的注入,後來發現是TempDbContext的建構子忘了呼叫base(options)

2018年3月15日 星期四

快速建立一個完整的React Application專案 (有路由 & 可呼叫後端API & 開發階段用webpack server)


整合以下連結內的教學建立一個專案:
1.[Webpack 4 Tutorial: from 0 Conf to Production Mode]
2.[React Redux Tutorial for Beginners: learning Redux in 2018]
3.[React Router v4 官方文件: Quick Start ]
4.[Redux Async Actions - Redux Tutorial #6]

安裝:
$ npm install --save-dev webpack webpack-cli webpack-dev-server babel-core babel-loader babel-preset-env babel-preset-react babel-plugin-transform-object-rest-spread html-webpack-plugin html-loader
$ npm install --save react react-dom react-router-dom prop-types redux react-redux redux-thunk redux-promise-middleware redux-logger axios

配置:
webpack.config.js
const HtmlWebPackPlugin = require('html-webpack-plugin');
module.exports = {
    devtool: 'inline-source-map',
    module: {
        rules: [
            {
                test: /\.js$/,
                exclude: /node_modules/,
                use: {
                    loader: 'babel-loader'
                }
            },
            {
                test: /\.html$/,
                use: [
                    {
                        loader: 'html-loader',
                        options: { minimize: true }
                    }
                ]
            }
        ]
    },
    plugins: [
        new HtmlWebPackPlugin({
            template: './src/index.html',
            filename: './index.html'
        })
    ]
};

.babelrc
{
    "presets": ["env", "react"],
    "plugins": ["transform-object-rest-spread"]
}

package.jsonscript
    "scripts": {
        "start": "webpack-dev-server --mode development --open",
        "build": "webpack --mode production"
    },

專案結構:
[src]
          [js]
                   [actions]
                   [components]
                   [constants]
                   [reducers]
                   [store]
          index.js
          index.html

備註:
想關掉webpack server時按下Ctrl+C,但是還是在running,要真的停止必須執行:
taskkill /F /IM node.exe

Redux 的   Middleware 套件說明:
1.      reduxdispatch只接受plain object,可使用redux-thunk來讓dispatch也可以接受function
2.      使用redux-promise-middleware可以讓action.payloadPromise類型來達到簡化設計三種狀態:Penging, Fulfilled, Rejected
3.      redux-promise-middleware可以和redux-thunk結合起來鍊接action
範例:使用者登入後取得他的文章
const mapDispatchToProps = dispatch => {
    return {
        login: (email, password) => dispatch(login(email, password))
    };
};

export const login = (email, password) => dispatch => {
    dispatch({
        type: 'LOGIN',
        payload: axios.post('http://localhost:5000/api/Token', {
            email,
            password
        })
    }).then(reponse => {
        dispatch({
            type: 'FETCH_POSTS',
            payload: axios.get('http://localhost:5000/api/posts')
        });
    });
};